From 5cca61a7f252fd485147f63477b3cfb75a5fbfce Mon Sep 17 00:00:00 2001 From: Gurge44 <90392754+Gurge44@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:08:01 +0200 Subject: [PATCH] Add files via upload --- GameMode/SoloKombatManager.cs | 403 +++ Modules/AURoleOptions.cs | 69 + Modules/AntiBlackout.cs | 158 + Modules/BanManager.cs | 177 ++ Modules/Camouflague.cs | 153 + Modules/ClientOptionItem.cs | 123 + Modules/Cloud.cs | 132 + Modules/CustomRoleSelector.cs | 368 +++ Modules/CustomRolesHelper.cs | 1174 ++++++++ Modules/CustomRpcSender.cs | 267 ++ Modules/CustomSounds.cs | 64 + Modules/CustomWinnerHolder.cs | 91 + Modules/DebugModeManager.cs | 33 + Modules/Debugger.cs | 114 + Modules/DevManager.cs | 102 + Modules/DisableDevice.cs | 181 ++ Modules/DoubleTrigger.cs | 77 + Modules/EAC.cs | 274 ++ Modules/ErrorText.cs | 168 ++ Modules/ExtendedPlayerControl.cs | 1042 +++++++ Modules/Extensions/IGameManagerEx.cs | 26 + .../GameOptionsSender/GameOptionsSender.cs | 95 + .../NormalGameOptionsSender.cs | 31 + .../PlayerGameOptionsSender.cs | 501 ++++ Modules/GameState.cs | 561 ++++ Modules/GuessManager.cs | 1249 ++++++++ Modules/HashAuth.cs | 66 + Modules/LateTask.cs | 54 + Modules/LocateArrow.cs | 151 + Modules/LogHandler/ILogHandler.cs | 13 + Modules/LogHandler/LogHandler.cs | 25 + Modules/MafiaRevengeManager.cs | 164 + Modules/MeetingTimeManager.cs | 79 + Modules/MessageManager/ICustomWriter.cs | 23 + Modules/MessageManager/WriterProxy.cs | 31 + Modules/ModUpdater.cs | 395 +++ Modules/NameColorManager.cs | 243 ++ Modules/NameNotifyManager.cs | 64 + Modules/OptionBackup/OptionBackupData.cs | 71 + Modules/OptionBackup/OptionBackupValue.cs | 81 + Modules/OptionHolder.cs | 2641 +++++++++++++++++ Modules/OptionItem/BooleanOptionItem.cs | 37 + Modules/OptionItem/FloatOptionItem.cs | 42 + Modules/OptionItem/IntegerOptionItem.cs | 42 + Modules/OptionItem/OptionItem.cs | 289 ++ Modules/OptionItem/PresetOptionItem.cs | 43 + Modules/OptionItem/StringOptionItem.cs | 58 + Modules/OptionItem/TextOptionItem.cs | 39 + Modules/OptionItem/ValueRule.cs | 78 + Modules/OptionShower.cs | 101 + Modules/RPC.cs | 1137 +++++++ Modules/Random/HashRandomWrapper.cs | 11 + Modules/Random/IRandom.cs | 43 + Modules/Random/MersenneTwister.cs | 132 + Modules/Random/NetRandomWrapper.cs | 21 + Modules/Random/Xorshift.cs | 36 + Modules/RetributionistRevengeManager.cs | 186 ++ Modules/ServerAddManager.cs | 61 + Modules/SpamManager.cs | 278 ++ Modules/TargetArrow.cs | 153 + Modules/TemplateManager.cs | 125 + Modules/Translator.cs | 182 ++ Modules/Utils.cs | 2223 ++++++++++++++ Modules/Zoom.cs | 104 + 64 files changed, 17155 insertions(+) create mode 100644 GameMode/SoloKombatManager.cs create mode 100644 Modules/AURoleOptions.cs create mode 100644 Modules/AntiBlackout.cs create mode 100644 Modules/BanManager.cs create mode 100644 Modules/Camouflague.cs create mode 100644 Modules/ClientOptionItem.cs create mode 100644 Modules/Cloud.cs create mode 100644 Modules/CustomRoleSelector.cs create mode 100644 Modules/CustomRolesHelper.cs create mode 100644 Modules/CustomRpcSender.cs create mode 100644 Modules/CustomSounds.cs create mode 100644 Modules/CustomWinnerHolder.cs create mode 100644 Modules/DebugModeManager.cs create mode 100644 Modules/Debugger.cs create mode 100644 Modules/DevManager.cs create mode 100644 Modules/DisableDevice.cs create mode 100644 Modules/DoubleTrigger.cs create mode 100644 Modules/EAC.cs create mode 100644 Modules/ErrorText.cs create mode 100644 Modules/ExtendedPlayerControl.cs create mode 100644 Modules/Extensions/IGameManagerEx.cs create mode 100644 Modules/GameOptionsSender/GameOptionsSender.cs create mode 100644 Modules/GameOptionsSender/NormalGameOptionsSender.cs create mode 100644 Modules/GameOptionsSender/PlayerGameOptionsSender.cs create mode 100644 Modules/GameState.cs create mode 100644 Modules/GuessManager.cs create mode 100644 Modules/HashAuth.cs create mode 100644 Modules/LateTask.cs create mode 100644 Modules/LocateArrow.cs create mode 100644 Modules/LogHandler/ILogHandler.cs create mode 100644 Modules/LogHandler/LogHandler.cs create mode 100644 Modules/MafiaRevengeManager.cs create mode 100644 Modules/MeetingTimeManager.cs create mode 100644 Modules/MessageManager/ICustomWriter.cs create mode 100644 Modules/MessageManager/WriterProxy.cs create mode 100644 Modules/ModUpdater.cs create mode 100644 Modules/NameColorManager.cs create mode 100644 Modules/NameNotifyManager.cs create mode 100644 Modules/OptionBackup/OptionBackupData.cs create mode 100644 Modules/OptionBackup/OptionBackupValue.cs create mode 100644 Modules/OptionHolder.cs create mode 100644 Modules/OptionItem/BooleanOptionItem.cs create mode 100644 Modules/OptionItem/FloatOptionItem.cs create mode 100644 Modules/OptionItem/IntegerOptionItem.cs create mode 100644 Modules/OptionItem/OptionItem.cs create mode 100644 Modules/OptionItem/PresetOptionItem.cs create mode 100644 Modules/OptionItem/StringOptionItem.cs create mode 100644 Modules/OptionItem/TextOptionItem.cs create mode 100644 Modules/OptionItem/ValueRule.cs create mode 100644 Modules/OptionShower.cs create mode 100644 Modules/RPC.cs create mode 100644 Modules/Random/HashRandomWrapper.cs create mode 100644 Modules/Random/IRandom.cs create mode 100644 Modules/Random/MersenneTwister.cs create mode 100644 Modules/Random/NetRandomWrapper.cs create mode 100644 Modules/Random/Xorshift.cs create mode 100644 Modules/RetributionistRevengeManager.cs create mode 100644 Modules/ServerAddManager.cs create mode 100644 Modules/SpamManager.cs create mode 100644 Modules/TargetArrow.cs create mode 100644 Modules/TemplateManager.cs create mode 100644 Modules/Translator.cs create mode 100644 Modules/Utils.cs create mode 100644 Modules/Zoom.cs diff --git a/GameMode/SoloKombatManager.cs b/GameMode/SoloKombatManager.cs new file mode 100644 index 000000000..b47a4a45f --- /dev/null +++ b/GameMode/SoloKombatManager.cs @@ -0,0 +1,403 @@ +using HarmonyLib; +using Hazel; +using System; +using System.Collections.Generic; +using System.Linq; +using TOHE.Roles.Neutral; +using UnityEngine; +using static TOHE.RandomSpawn; + +namespace TOHE; + +internal static class SoloKombatManager +{ + private static Dictionary PlayerHPMax = new(); + private static Dictionary PlayerHP = new(); + private static Dictionary PlayerHPReco = new(); + private static Dictionary PlayerATK = new(); + private static Dictionary PlayerDF = new(); + + public static bool SoloAlive(this PlayerControl pc) => pc.HP() > 0f; + + public static float HPMAX(this PlayerControl pc) => PlayerHPMax[pc.PlayerId]; + public static float HP(this PlayerControl pc) => PlayerHP[pc.PlayerId]; + public static float HPRECO(this PlayerControl pc) => PlayerHPReco[pc.PlayerId]; + public static float ATK(this PlayerControl pc) => PlayerATK[pc.PlayerId]; + public static float DF(this PlayerControl pc) => PlayerDF[pc.PlayerId]; + + private static Dictionary originalSpeed = new(); + public static Dictionary KBScore = new(); + public static int RoundTime = new(); + + //Options + public static OptionItem KB_GameTime; + public static OptionItem KB_ATKCooldown; + public static OptionItem KB_HPMax; + public static OptionItem KB_ATK; + public static OptionItem KB_RecoverAfterSecond; + public static OptionItem KB_RecoverPerSecond; + public static OptionItem KB_ResurrectionWaitingTime; + public static OptionItem KB_KillBonusMultiplier; + public static OptionItem KB_BootVentWhenDead; + + public static void SetupCustomOption() + { + KB_GameTime = IntegerOptionItem.Create(66_233_001, "KB_GameTime", new(30, 300, 5), 180, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetHeader(true); + KB_ATKCooldown = FloatOptionItem.Create(66_223_008, "KB_ATKCooldown", new(1f, 10f, 0.1f), 1f, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + KB_HPMax = FloatOptionItem.Create(66_233_002, "KB_HPMax", new(10f, 990f, 5f), 100f, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Health); + KB_ATK = FloatOptionItem.Create(66_233_003, "KB_ATK", new(1f, 100f, 1f), 8f, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Health); + KB_RecoverPerSecond = FloatOptionItem.Create(66_233_005, "KB_RecoverPerSecond", new(1f, 180f, 1f), 2f, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Health); + KB_RecoverAfterSecond = IntegerOptionItem.Create(66_233_004, "KB_RecoverAfterSecond", new(0, 60, 1), 8, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + KB_ResurrectionWaitingTime = IntegerOptionItem.Create(66_233_006, "KB_ResurrectionWaitingTime", new(3, 990, 1), 15, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds); + KB_KillBonusMultiplier = FloatOptionItem.Create(66_233_007, "KB_KillBonusMultiplier", new(0.25f, 5f, 0.25f), 1.25f, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)) + .SetValueFormat(OptionFormat.Multiplier); + KB_BootVentWhenDead = BooleanOptionItem.Create(66_233_009, "KB_BootVentWhenDead", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.SoloKombat) + .SetColor(new Color32(245, 82, 82, byte.MaxValue)); + } + + public static void Init() + { + if (Options.CurrentGameMode != CustomGameMode.SoloKombat) return; + + PlayerHPMax = new(); + PlayerHP = new(); + PlayerHPReco = new(); + PlayerATK = new(); + PlayerDF = new(); + + LastHurt = new(); + originalSpeed = new(); + BackCountdown = new(); + KBScore = new(); + RoundTime = KB_GameTime.GetInt() + 8; + + foreach (var pc in Main.AllAlivePlayerControls) + { + PlayerHPMax.TryAdd(pc.PlayerId, KB_HPMax.GetFloat()); + PlayerHP.TryAdd(pc.PlayerId, KB_HPMax.GetFloat()); + PlayerHPReco.TryAdd(pc.PlayerId, KB_RecoverPerSecond.GetFloat()); + PlayerATK.TryAdd(pc.PlayerId, KB_ATK.GetFloat()); + PlayerDF.TryAdd(pc.PlayerId, 0f); + + KBScore.TryAdd(pc.PlayerId, 0); + + LastHurt.TryAdd(pc.PlayerId, Utils.GetTimeStamp()); + } + } + private static void SendRPCSyncKBBackCountdown(PlayerControl player) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncKBBackCountdown, SendOption.Reliable, player.GetClientId()); + int x = BackCountdown.TryGetValue(player.PlayerId, out var value) ? value : -1; + writer.Write(x); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPCSyncBackCountdown(MessageReader reader) + { + int num = reader.ReadInt32(); + if (num == -1) + BackCountdown.Remove(PlayerControl.LocalPlayer.PlayerId); + else + { + BackCountdown.TryAdd(PlayerControl.LocalPlayer.PlayerId, num); + BackCountdown[PlayerControl.LocalPlayer.PlayerId] = num; + } + } + private static void SendRPCSyncKBPlayer(byte playerId) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncKBPlayer, SendOption.Reliable, -1); + writer.Write(playerId); + writer.Write(PlayerHPMax[playerId]); + writer.Write(PlayerHP[playerId]); + writer.Write(PlayerHPReco[playerId]); + writer.Write(PlayerATK[playerId]); + writer.Write(PlayerDF[playerId]); + writer.Write(KBScore[playerId]); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPCSyncKBPlayer(MessageReader reader) + { + byte PlayerId = reader.ReadByte(); + PlayerHPMax[PlayerId] = reader.ReadSingle(); + PlayerHP[PlayerId] = reader.ReadSingle(); + PlayerHPReco[PlayerId] = reader.ReadSingle(); + PlayerATK[PlayerId] = reader.ReadSingle(); + PlayerDF[PlayerId] = reader.ReadSingle(); + KBScore[PlayerId] = reader.ReadInt32(); + } + public static void SendRPCSyncNameNotify(PlayerControl pc) + { + if (pc.AmOwner || !pc.IsModClient()) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncKBNameNotify, SendOption.Reliable, pc.GetClientId()); + if (NameNotify.ContainsKey(pc.PlayerId)) + writer.Write(NameNotify[pc.PlayerId].Item1); + else writer.Write(""); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPCSyncNameNotify(MessageReader reader) + { + var name = reader.ReadString(); + NameNotify.Remove(PlayerControl.LocalPlayer.PlayerId); + if (name != null && name != "") + NameNotify.Add(PlayerControl.LocalPlayer.PlayerId, (name, 0)); + } + public static string GetDisplayHealth(PlayerControl pc) + => pc.SoloAlive() ? Utils.ColorString(GetHealthColor(pc), $"{(int)pc.HP()}/{(int)pc.HPMAX()}") : ""; + private static Color32 GetHealthColor(PlayerControl pc) + { + var x = (int)(pc.HP() / pc.HPMAX() * 10 * 50); + int R = 255; int G = 255; int B = 0; + if (x > 255) R -= (x - 255); else G = x; + return new Color32((byte)R, (byte)G, (byte)B, byte.MaxValue); + } + public static Dictionary NameNotify = new(); + public static void GetNameNotify(PlayerControl player, ref string name) + { + if (Options.CurrentGameMode != CustomGameMode.SoloKombat || player == null) return; + if (BackCountdown.ContainsKey(player.PlayerId)) + { + name = string.Format(Translator.GetString("KBBackCountDown"), BackCountdown[player.PlayerId]); + NameNotify.Remove(player.PlayerId); + return; + } + if (NameNotify.ContainsKey(player.PlayerId)) + { + name = NameNotify[player.PlayerId].Item1; + return; + } + } + public static string GetDisplayScore(byte playerId) + { + int rank = GetRankOfScore(playerId); + string score = KBScore.TryGetValue(playerId, out var s) ? $"{s}" : "Invalid"; + string text = string.Format(Translator.GetString("KBDisplayScore"), rank.ToString(), score); + Color color = Utils.GetRoleColor(CustomRoles.KB_Normal); + return Utils.ColorString(color, text); + } + public static int GetRankOfScore(byte playerId) + { + try + { + int ms = KBScore[playerId]; + int rank = 1 + KBScore.Values.Where(x => x > ms).Count(); + rank += KBScore.Where(x => x.Value == ms).ToList().IndexOf(new(playerId, ms)); + return rank; + } + catch + { + return Main.AllPlayerControls.Count(); + } + } + public static string GetHudText() + { + return string.Format(Translator.GetString("KBTimeRemain"), RoundTime.ToString()); + } + public static void OnPlayerAttack(PlayerControl killer, PlayerControl target) + { + if (killer == null || target == null || Options.CurrentGameMode != CustomGameMode.SoloKombat) return; + if (!killer.SoloAlive() || !target.SoloAlive() || target.inVent) return; + + var dmg = killer.ATK() - target.DF(); + PlayerHP[target.PlayerId] = Math.Max(0f, target.HP() - dmg); + + if (!target.SoloAlive()) + { + OnPlyaerDead(target); + OnPlayerKill(killer); + } + + LastHurt[target.PlayerId] = Utils.GetTimeStamp(); + + killer.SetKillCooldownV2(1f, target); + RPC.PlaySoundRPC(killer.PlayerId, Sounds.KillSound); + RPC.PlaySoundRPC(target.PlayerId, Sounds.KillSound); + if (!target.IsModClient() && !target.AmOwner) + target.RpcGuardAndKill(colorId: 5); + + SendRPCSyncKBPlayer(target.PlayerId); + Utils.NotifyRoles(SpecifySeer: killer); + Utils.NotifyRoles(SpecifySeer: target); + } + public static void OnPlayerBack(PlayerControl pc) + { + BackCountdown.Remove(pc.PlayerId); + PlayerHP[pc.PlayerId] = pc.HPMAX(); + SendRPCSyncKBPlayer(pc.PlayerId); + + LastHurt[pc.PlayerId] = Utils.GetTimeStamp(); + Main.AllPlayerSpeed[pc.PlayerId] = Main.AllPlayerSpeed[pc.PlayerId] - 0.3f + originalSpeed[pc.PlayerId]; + pc.MarkDirtySettings(); + + RPC.PlaySoundRPC(pc.PlayerId, Sounds.TaskComplete); + pc.RpcGuardAndKill(colorId: 1); + + PlayerRandomSpwan(pc); + } + public static void PlayerRandomSpwan(PlayerControl pc) + { + SpawnMap map; + switch (Main.NormalOptions.MapId) + { + case 0: + map = new SkeldSpawnMap(); + map.RandomTeleport(pc); + break; + case 1: + map = new MiraHQSpawnMap(); + map.RandomTeleport(pc); + break; + case 2: + map = new PolusSpawnMap(); + map.RandomTeleport(pc); + break; + } + } + public static void OnPlyaerDead(PlayerControl target) + { + originalSpeed.Remove(target.PlayerId); + originalSpeed.Add(target.PlayerId, Main.AllPlayerSpeed[target.PlayerId]); + + Utils.TP(target.NetTransform, Pelican.GetBlackRoomPS()); + Main.AllPlayerSpeed[target.PlayerId] = 0.3f; + target.MarkDirtySettings(); + + BackCountdown.TryAdd(target.PlayerId, KB_ResurrectionWaitingTime.GetInt()); + SendRPCSyncKBBackCountdown(target); + } + public static void OnPlayerKill(PlayerControl killer) + { + killer.KillFlash(); + if (PlayerControl.LocalPlayer.Is(CustomRoles.GM)) + PlayerControl.LocalPlayer.KillFlash(); + + KBScore[killer.PlayerId]++; + + float addRate = IRandom.Instance.Next(3, 5 + GetRankOfScore(killer.PlayerId)) / 100f; + addRate *= KB_KillBonusMultiplier.GetFloat(); + float addin; + switch (IRandom.Instance.Next(0, 3)) + { + case 0: + addin = killer.HPMAX() * addRate; + PlayerHPMax[killer.PlayerId] += addin; + AddNameNotify(killer, string.Format(Translator.GetString("KB_Buff_HPMax"), addin.ToString("0.0#####"))); + break; + case 1: + addin = killer.HPRECO() * addRate * 2; + PlayerHPReco[killer.PlayerId] += addin; + AddNameNotify(killer, string.Format(Translator.GetString("KB_Buff_HPReco"), addin.ToString("0.0#####"))); + break; + case 2: + addin = killer.ATK() * addRate; + PlayerATK[killer.PlayerId] += addin; + AddNameNotify(killer, string.Format(Translator.GetString("KB_Buff_ATK"), addin.ToString("0.0#####"))); + break; + } + } + public static void AddNameNotify(PlayerControl pc, string text, int time = 5) + { + NameNotify.Remove(pc.PlayerId); + NameNotify.Add(pc.PlayerId, (text, Utils.GetTimeStamp() + time)); + SendRPCSyncNameNotify(pc); + SendRPCSyncKBPlayer(pc.PlayerId); + Utils.NotifyRoles(SpecifySeer: pc); + } + + private static Dictionary BackCountdown = new(); + private static Dictionary LastHurt = new(); + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))] + class FixedUpdatePatch + { + private static long LastFixedUpdate = new(); + public static void Postfix(PlayerControl __instance) + { + if (!GameStates.IsInTask || Options.CurrentGameMode != CustomGameMode.SoloKombat) return; + + if (AmongUsClient.Instance.AmHost) + { + foreach (var pc in Main.AllPlayerControls) + { + // 锁定死亡玩家在小黑屋 + if (!pc.SoloAlive()) + { + if (pc.inVent && KB_BootVentWhenDead.GetBool()) pc.MyPhysics.RpcExitVent(2); + var pos = Pelican.GetBlackRoomPS(); + var dis = Vector2.Distance(pos, pc.GetTruePosition()); + if (dis > 1f) Utils.TP(pc.NetTransform, pos); + } + } + + if (LastFixedUpdate == Utils.GetTimeStamp()) return; + LastFixedUpdate = Utils.GetTimeStamp(); + + // 减少全局倒计时 + RoundTime--; + + if (!AmongUsClient.Instance.AmHost) return; + + foreach (var pc in Main.AllPlayerControls) + { + bool notifyRoles = false; + // 每秒回复血量 + if (LastHurt[pc.PlayerId] + KB_RecoverAfterSecond.GetInt() < Utils.GetTimeStamp() && pc.HP() < pc.HPMAX() && pc.SoloAlive() && !pc.inVent) + { + PlayerHP[pc.PlayerId] += pc.HPRECO(); + PlayerHP[pc.PlayerId] = Math.Min(pc.HPMAX(), pc.HP()); + SendRPCSyncKBPlayer(pc.PlayerId); + notifyRoles = true; + } + // 复活玩家随机复活(二次确认) + if (pc.SoloAlive() && !pc.inVent) + { + var pos = Pelican.GetBlackRoomPS(); + var dis = Vector2.Distance(pos, pc.GetTruePosition()); + if (dis < 1.1f) PlayerRandomSpwan(pc); + } + // 复活倒计时 + if (BackCountdown.ContainsKey(pc.PlayerId)) + { + BackCountdown[pc.PlayerId]--; + if (BackCountdown[pc.PlayerId] <= 0) + OnPlayerBack(pc); + SendRPCSyncKBBackCountdown(pc); + notifyRoles = true; + } + // 清除过期的提示信息 + if (NameNotify.ContainsKey(pc.PlayerId) && NameNotify[pc.PlayerId].Item2 < Utils.GetTimeStamp()) + { + NameNotify.Remove(pc.PlayerId); + SendRPCSyncNameNotify(pc); + notifyRoles = true; + } + // 必要时刷新玩家名字 + if (notifyRoles) Utils.NotifyRoles(SpecifySeer: pc); + } + } + } + } +} \ No newline at end of file diff --git a/Modules/AURoleOptions.cs b/Modules/AURoleOptions.cs new file mode 100644 index 000000000..5bf37f25d --- /dev/null +++ b/Modules/AURoleOptions.cs @@ -0,0 +1,69 @@ +using AmongUs.GameOptions; + +namespace TOHE; + +public static class AURoleOptions +{ + private static IGameOptions Opt; + public static void SetOpt(IGameOptions opt) => Opt = opt; + public static float KillCooldown + { + get => Opt.GetFloat(FloatOptionNames.KillCooldown); + set => Opt.SetFloat(FloatOptionNames.KillCooldown, value); + } + public static float PlayerSpeedMod + { + get => Opt.GetFloat(FloatOptionNames.PlayerSpeedMod); + set => Opt.SetFloat(FloatOptionNames.PlayerSpeedMod, value); + } + public static float ScientistCooldown + { + get => Opt.GetFloat(FloatOptionNames.ScientistCooldown); + set => Opt.SetFloat(FloatOptionNames.ScientistCooldown, value); + } + public static float ScientistBatteryCharge + { + get => Opt.GetFloat(FloatOptionNames.ScientistBatteryCharge); + set => Opt.SetFloat(FloatOptionNames.ScientistBatteryCharge, value); + } + public static float EngineerCooldown + { + get => Opt.GetFloat(FloatOptionNames.EngineerCooldown); + set => Opt.SetFloat(FloatOptionNames.EngineerCooldown, value); + } + public static float EngineerInVentMaxTime + { + get => Opt.GetFloat(FloatOptionNames.EngineerInVentMaxTime); + set => Opt.SetFloat(FloatOptionNames.EngineerInVentMaxTime, value); + } + public static float GuardianAngelCooldown + { + get => Opt.GetFloat(FloatOptionNames.GuardianAngelCooldown); + set => Opt.SetFloat(FloatOptionNames.GuardianAngelCooldown, value); + } + public static float ProtectionDurationSeconds + { + get => Opt.GetFloat(FloatOptionNames.ProtectionDurationSeconds); + set => Opt.SetFloat(FloatOptionNames.ProtectionDurationSeconds, value); + } + public static bool ImpostorsCanSeeProtect + { + get => Opt.GetBool(BoolOptionNames.ImpostorsCanSeeProtect); + set => Opt.SetBool(BoolOptionNames.ImpostorsCanSeeProtect, value); + } + public static float ShapeshifterDuration + { + get => Opt.GetFloat(FloatOptionNames.ShapeshifterDuration); + set => Opt.SetFloat(FloatOptionNames.ShapeshifterDuration, value); + } + public static float ShapeshifterCooldown + { + get => Opt.GetFloat(FloatOptionNames.ShapeshifterCooldown); + set => Opt.SetFloat(FloatOptionNames.ShapeshifterCooldown, value); + } + public static bool ShapeshifterLeaveSkin + { + get => Opt.GetBool(BoolOptionNames.ShapeshifterLeaveSkin); + set => Opt.SetBool(BoolOptionNames.ShapeshifterLeaveSkin, value); + } +} \ No newline at end of file diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs new file mode 100644 index 000000000..d8b8f62a4 --- /dev/null +++ b/Modules/AntiBlackout.cs @@ -0,0 +1,158 @@ +using AmongUs.GameOptions; +using Hazel; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using TOHE.Modules; +using TOHE.Roles.Neutral; + +namespace TOHE; + +public static class AntiBlackout +{ + /// + ///追放処理を上書きするかどうか + /// + public static bool OverrideExiledPlayer => IsRequired && (IsSingleImpostor || Diff_CrewImp == 1); + /// + ///インポスターが一人しか存在しない設定かどうか + /// + public static bool IsSingleImpostor => (Main.RealOptionsData != null ? Main.RealOptionsData.GetInt(Int32OptionNames.NumImpostors) : Main.NormalOptions.NumImpostors) <= 1; + /// + ///AntiBlackout内の処理が必要であるかどうか + /// + public static bool IsRequired => Options.NoGameEnd.GetBool() + || Jackal.IsEnable || Pelican.IsEnable + || Gamer.IsEnable || BloodKnight.IsEnable + || Succubus.IsEnable || Poisoner.IsEnable + || Infectious.IsEnable || Juggernaut.IsEnable + || Ritualist.IsEnable || Virus.IsEnable + || Wraith.IsEnable || HexMaster.IsEnable + || Traitor.IsEnable || Pickpocket.IsEnable + || NSerialKiller.IsEnable || NWitch.IsEnable + || Maverick.IsEnable || Jinx.IsEnable + || Medusa.IsEnable || Spiritcaller.IsEnable + || PlagueBearer.IsEnable || CustomRoles.Sidekick.RoleExist() + || CustomRoles.Pestilence.RoleExist() || CustomRoles.Arsonist.RoleExist() + || Pirate.IsEnable; + /// + ///インポスター以外の人数とインポスターの人数の差 + /// + public static int Diff_CrewImp + { + get + { + int numImpostors = 0; + int numCrewmates = 0; + foreach (var pc in Main.AllPlayerControls) + { + if (pc.Data.Role.IsImpostor) numImpostors++; + else numCrewmates++; + } + return numCrewmates - numImpostors; + } + } + public static bool IsCached { get; private set; } = false; + private static Dictionary isDeadCache = new(); + private readonly static LogHandler logger = Logger.Handler("AntiBlackout"); + + public static void SetIsDead(bool doSend = true, [CallerMemberName] string callerMethodName = "") + { + logger.Info($"SetIsDead is called from {callerMethodName}"); + if (IsCached) + { + logger.Info("再度SetIsDeadを実行する前に、RestoreIsDeadを実行してください。"); + return; + } + isDeadCache.Clear(); + foreach (var info in GameData.Instance.AllPlayers) + { + if (info == null) continue; + isDeadCache[info.PlayerId] = (info.IsDead, info.Disconnected); + info.IsDead = false; + info.Disconnected = false; + } + IsCached = true; + if (doSend) SendGameData(); + } + public static void RestoreIsDead(bool doSend = true, [CallerMemberName] string callerMethodName = "") + { + logger.Info($"RestoreIsDead is called from {callerMethodName}"); + foreach (var info in GameData.Instance.AllPlayers) + { + if (info == null) continue; + if (isDeadCache.TryGetValue(info.PlayerId, out var val)) + { + info.IsDead = val.isDead; + info.Disconnected = val.Disconnected; + } + } + isDeadCache.Clear(); + IsCached = false; + if (doSend) SendGameData(); + } + + public static void SendGameData([CallerMemberName] string callerMethodName = "") + { + logger.Info($"SendGameData is called from {callerMethodName}"); + MessageWriter writer = MessageWriter.Get(SendOption.Reliable); + // 書き込み {}は読みやすさのためです。 + writer.StartMessage(5); //0x05 GameData + { + writer.Write(AmongUsClient.Instance.GameId); + writer.StartMessage(1); //0x01 Data + { + writer.WritePacked(GameData.Instance.NetId); + GameData.Instance.Serialize(writer, true); + + } + writer.EndMessage(); + } + writer.EndMessage(); + + AmongUsClient.Instance.SendOrDisconnect(writer); + writer.Recycle(); + } + public static void OnDisconnect(GameData.PlayerInfo player) + { + // 実行条件: クライアントがホストである, IsDeadが上書きされている, playerが切断済み + if (!AmongUsClient.Instance.AmHost || !IsCached || !player.Disconnected) return; + isDeadCache[player.PlayerId] = (true, true); + player.IsDead = player.Disconnected = false; + SendGameData(); + } + + /// + ///一時的にIsDeadを本来のものに戻した状態でコードを実行します + ///実行内容 + /// + public static void TempRestore(Action action) + { + logger.Info("==Temp Restore=="); + //IsDeadが上書きされた状態でTempRestoreが実行されたかどうか + bool before_IsCached = IsCached; + try + { + if (before_IsCached) RestoreIsDead(doSend: false); + action(); + } + catch (Exception ex) + { + logger.Warn("AntiBlackout.TempRestore内で例外が発生しました"); + logger.Exception(ex); + } + finally + { + if (before_IsCached) SetIsDead(doSend: false); + logger.Info("==/Temp Restore=="); + } + } + + public static void Reset() + { + logger.Info("==Reset=="); + if (isDeadCache == null) isDeadCache = new(); + isDeadCache.Clear(); + IsCached = false; + } +} \ No newline at end of file diff --git a/Modules/BanManager.cs b/Modules/BanManager.cs new file mode 100644 index 000000000..67be87290 --- /dev/null +++ b/Modules/BanManager.cs @@ -0,0 +1,177 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using static TOHE.Translator; + +namespace TOHE; + +public static class BanManager +{ + private static readonly string DENY_NAME_LIST_PATH = @"./TOHE_DATA/DenyName.txt"; + private static readonly string BAN_LIST_PATH = @"./TOHE_DATA/BanList.txt"; + private static readonly string MODERATOR_LIST_PATH = @"./TOHE_DATA/Moderators.txt"; + private static List EACList = new(); + public static void Init() + { + try + { + Directory.CreateDirectory("TOHE_DATA"); + + if (!File.Exists(BAN_LIST_PATH)) + { + Logger.Warn("创建新的 BanList.txt 文件", "BanManager"); + File.Create(BAN_LIST_PATH).Close(); + } + if (!File.Exists(DENY_NAME_LIST_PATH)) + { + Logger.Warn("创建新的 DenyName.txt 文件", "BanManager"); + File.Create(DENY_NAME_LIST_PATH).Close(); + File.WriteAllText(DENY_NAME_LIST_PATH, GetResourcesTxt("TOHE.Resources.Config.DenyName.txt")); + } + if (!File.Exists(MODERATOR_LIST_PATH)) + { + Logger.Warn("Creating a new Moderators.txt file", "BanManager"); + File.Create(MODERATOR_LIST_PATH).Close(); + File.WriteAllText(MODERATOR_LIST_PATH, GetResourcesTxt("TOHE.Resources.Config.Moderators.txt")); + } + + //读取EAC名单 + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("TOHE.Resources.Config.EACList.txt"); + stream.Position = 0; + using StreamReader sr = new(stream, Encoding.UTF8); + string line; + while ((line = sr.ReadLine()) != null) + { + if (line == "" || line.StartsWith("#")) continue; + // if (line.Contains("actorour#0029")) continue; + if (line.Contains("gnuedaphic#7196")) continue; + EACList.Add(line); + } + + } + catch (Exception ex) + { + Logger.Exception(ex, "BanManager"); + } + } + private static string GetResourcesTxt(string path) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + stream.Position = 0; + using StreamReader reader = new(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + public static void AddBanPlayer(InnerNet.ClientData player) + { + if (!AmongUsClient.Instance.AmHost || player == null) return; + if (!CheckBanList(player?.FriendCode) && player.FriendCode != "") + { + File.AppendAllText(BAN_LIST_PATH, $"{player.FriendCode},{player.PlayerName}\n"); + Logger.SendInGame(string.Format(GetString("Message.AddedPlayerToBanList"), player.PlayerName)); + } + } + public static void CheckDenyNamePlayer(InnerNet.ClientData player) + { + if (!AmongUsClient.Instance.AmHost || !Options.ApplyDenyNameList.GetBool()) return; + try + { + Directory.CreateDirectory("TOHE_DATA"); + if (!File.Exists(DENY_NAME_LIST_PATH)) File.Create(DENY_NAME_LIST_PATH).Close(); + using StreamReader sr = new(DENY_NAME_LIST_PATH); + string line; + while ((line = sr.ReadLine()) != null) + { + if (line == "") continue; + // if (line.Contains("actorour#0029")) continue; + if (line.Contains("gnuedaphic#7196")) continue; + if (line.Contains("Amogus")) + { + AmongUsClient.Instance.KickPlayer(player.Id, false); + Logger.SendInGame(string.Format(GetString("Message.KickedByDenyName"), player.PlayerName, line)); + Logger.Info($"{player.PlayerName}は名前が「{line}」に一致したためキックされました。", "Kick"); + return; + } + if (line.Contains("Amogus V")) + { + AmongUsClient.Instance.KickPlayer(player.Id, false); + Logger.SendInGame(string.Format(GetString("Message.KickedByDenyName"), player.PlayerName, line)); + Logger.Info($"{player.PlayerName}は名前が「{line}」に一致したためキックされました。", "Kick"); + return; + } + + if (Regex.IsMatch(player.PlayerName, line)) + { + AmongUsClient.Instance.KickPlayer(player.Id, false); + Logger.SendInGame(string.Format(GetString("Message.KickedByDenyName"), player.PlayerName, line)); + Logger.Info($"{player.PlayerName}は名前が「{line}」に一致したためキックされました。", "Kick"); + return; + } + } + } + catch (Exception ex) + { + Logger.Exception(ex, "CheckDenyNamePlayer"); + } + } + public static void CheckBanPlayer(InnerNet.ClientData player) + { + if (!AmongUsClient.Instance.AmHost || !Options.ApplyBanList.GetBool()) return; + if (CheckBanList(player?.FriendCode)) + { + AmongUsClient.Instance.KickPlayer(player.Id, true); + Logger.SendInGame(string.Format(GetString("Message.BanedByBanList"), player.PlayerName)); + Logger.Info($"{player.PlayerName}は過去にBAN済みのためBANされました。", "BAN"); + return; + } + if (CheckEACList(player?.FriendCode)) + { + AmongUsClient.Instance.KickPlayer(player.Id, true); + Logger.SendInGame(string.Format(GetString("Message.BanedByEACList"), player.PlayerName)); + Logger.Info($"{player.PlayerName}存在于EAC封禁名单", "BAN"); + return; + } + } + public static bool CheckBanList(string code) + { + if (code == "") return false; + try + { + Directory.CreateDirectory("TOHE_DATA"); + if (!File.Exists(BAN_LIST_PATH)) File.Create(BAN_LIST_PATH).Close(); + using StreamReader sr = new(BAN_LIST_PATH); + string line; + while ((line = sr.ReadLine()) != null) + { + if (line == "") continue; + // if (line.Contains("actorour#0029")) continue; + if (line.Contains("gnuedaphic#7196")) continue; + if (line.Contains(code)) return true; + } + } + catch (Exception ex) + { + Logger.Exception(ex, "CheckBanList"); + } + return false; + } + public static bool CheckEACList(string code) + { + if (code == "") return false; + return EACList.Any(x => x.Contains(code)); + } +} +[HarmonyPatch(typeof(BanMenu), nameof(BanMenu.Select))] +class BanMenuSelectPatch +{ + public static void Postfix(BanMenu __instance, int clientId) + { + InnerNet.ClientData recentClient = AmongUsClient.Instance.GetRecentClient(clientId); + if (recentClient == null) return; + if (!BanManager.CheckBanList(recentClient?.FriendCode)) __instance.BanButton.GetComponent().SetEnabledColors(); + } +} \ No newline at end of file diff --git a/Modules/Camouflague.cs b/Modules/Camouflague.cs new file mode 100644 index 000000000..194c53c2c --- /dev/null +++ b/Modules/Camouflague.cs @@ -0,0 +1,153 @@ +using AmongUs.Data; +using HarmonyLib; +using System.Collections.Generic; +using TOHE.Roles.Impostor; + +namespace TOHE; + +static class PlayerOutfitExtension +{ + public static GameData.PlayerOutfit Set(this GameData.PlayerOutfit instance, string playerName, int colorId, string hatId, string skinId, string visorId, string petId) + { + instance.PlayerName = playerName; + instance.ColorId = colorId; + instance.HatId = hatId; + instance.SkinId = skinId; + instance.VisorId = visorId; + instance.PetId = petId; + return instance; + } + public static bool Compare(this GameData.PlayerOutfit instance, GameData.PlayerOutfit targetOutfit) + { + return instance.ColorId == targetOutfit.ColorId && + instance.HatId == targetOutfit.HatId && + instance.SkinId == targetOutfit.SkinId && + instance.VisorId == targetOutfit.VisorId && + instance.PetId == targetOutfit.PetId; + + } + public static string GetString(this GameData.PlayerOutfit instance) + { + return $"{instance.PlayerName} Color:{instance.ColorId} {instance.HatId} {instance.SkinId} {instance.VisorId} {instance.PetId}"; + } +} +public static class Camouflage +{ + static GameData.PlayerOutfit CamouflageOutfit = new GameData.PlayerOutfit().Set("", 15, "", "", "", ""); // Default + + public static bool IsCamouflage; + public static Dictionary PlayerSkins = new(); + + public static void Init() + { + IsCamouflage = false; + PlayerSkins.Clear(); + + switch (Options.KPDCamouflageMode.GetValue()) + { + case 0: // Default + CamouflageOutfit = new GameData.PlayerOutfit() + .Set("", 15, "", "", "", ""); + break; + + case 1: // Host's outfit + CamouflageOutfit = new GameData.PlayerOutfit() + .Set("", DataManager.Player.Customization.Color, DataManager.Player.Customization.Hat, DataManager.Player.Customization.Skin, DataManager.Player.Customization.Visor, DataManager.Player.Customization.Pet); + break; + + case 2: // Karpe + CamouflageOutfit = new GameData.PlayerOutfit() + .Set("", 13, "hat_pk05_Plant", "", "visor_BubbleBumVisor", ""); + break; + + case 3: // Loonie + CamouflageOutfit = new GameData.PlayerOutfit() + .Set("", 12, "hat_pkHW01_Wolf", "skin_scarfskin", "visor_Carrot", "pet_Pusheen"); + break; + + case 4: // Lauryn + CamouflageOutfit = new GameData.PlayerOutfit() + .Set("", 13, "hat_rabbitEars", "skin_Bananaskin", "visor_BubbleBumVisor", "pet_Pusheen"); + break; + + case 5: // Moe + CamouflageOutfit = new GameData.PlayerOutfit() + .Set("", 4, "hat_mira_headset_yellow", "skin_SuitB", "visor_lollipopCrew", "pet_EmptyPet"); + break; + } + } + public static void CheckCamouflage() + { + if (!(AmongUsClient.Instance.AmHost && (Options.CommsCamouflage.GetBool() || CustomRoles.Camouflager.IsEnable()))) return; + + var oldIsCamouflage = IsCamouflage; + + IsCamouflage = (Utils.IsActive(SystemTypes.Comms) && Options.CommsCamouflage.GetBool()) || Camouflager.IsActive; + + if (oldIsCamouflage != IsCamouflage) + { + Main.AllPlayerControls.Do(pc => RpcSetSkin(pc)); + Utils.NotifyRoles(); + } + } + public static void RpcSetSkin(PlayerControl target, bool ForceRevert = false, bool RevertToDefault = false) + { + if (!(AmongUsClient.Instance.AmHost && (Options.CommsCamouflage.GetBool() || CustomRoles.Camouflager.IsEnable()))) return; + if (target == null) return; + + var id = target.PlayerId; + + if (IsCamouflage) + { + //コミュサボ中 + + //死んでいたら処理しない + if (Main.PlayerStates[id].IsDead) return; + } + + var newOutfit = CamouflageOutfit; + + if (!IsCamouflage || ForceRevert) + { + //コミュサボ解除または強制解除 + + if (Main.CheckShapeshift.TryGetValue(id, out var shapeshifting) && shapeshifting && !RevertToDefault) + { + //シェイプシフターなら今の姿のidに変更 + id = Main.ShapeshiftTarget[id]; + } + + newOutfit = PlayerSkins[id]; + } + Logger.Info($"newOutfit={newOutfit.GetString()}", "RpcSetSkin"); + + var sender = CustomRpcSender.Create(name: $"Camouflage.RpcSetSkin({target.Data.PlayerName})"); + + target.SetColor(newOutfit.ColorId); + sender.AutoStartRpc(target.NetId, (byte)RpcCalls.SetColor) + .Write(newOutfit.ColorId) + .EndRpc(); + + target.SetHat(newOutfit.HatId, newOutfit.ColorId); + sender.AutoStartRpc(target.NetId, (byte)RpcCalls.SetHatStr) + .Write(newOutfit.HatId) + .EndRpc(); + + target.SetSkin(newOutfit.SkinId, newOutfit.ColorId); + sender.AutoStartRpc(target.NetId, (byte)RpcCalls.SetSkinStr) + .Write(newOutfit.SkinId) + .EndRpc(); + + target.SetVisor(newOutfit.VisorId, newOutfit.ColorId); + sender.AutoStartRpc(target.NetId, (byte)RpcCalls.SetVisorStr) + .Write(newOutfit.VisorId) + .EndRpc(); + + target.SetPet(newOutfit.PetId); + sender.AutoStartRpc(target.NetId, (byte)RpcCalls.SetPetStr) + .Write(newOutfit.PetId) + .EndRpc(); + + sender.SendMessage(); + } +} \ No newline at end of file diff --git a/Modules/ClientOptionItem.cs b/Modules/ClientOptionItem.cs new file mode 100644 index 000000000..69f80969d --- /dev/null +++ b/Modules/ClientOptionItem.cs @@ -0,0 +1,123 @@ +using BepInEx.Configuration; +using System; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace TOHE; + +//来源:https://github.com/tukasa0001/TownOfHost/pull/1265 +public class ClientOptionItem +{ + public ConfigEntry Config; + public ToggleButtonBehaviour ToggleButton; + + public static SpriteRenderer CustomBackground; + private static int numOptions = 0; + + private ClientOptionItem( + string name, + ConfigEntry config, + OptionsMenuBehaviour optionsMenuBehaviour, + Action additionalOnClickAction = null) + { + try + { + Config = config; + + var mouseMoveToggle = optionsMenuBehaviour.DisableMouseMovement; + + // 1つ目のボタンの生成時に背景も生成 + if (CustomBackground == null) + { + numOptions = 0; + CustomBackground = Object.Instantiate(optionsMenuBehaviour.Background, optionsMenuBehaviour.transform); + CustomBackground.name = "CustomBackground"; + CustomBackground.transform.localScale = new(0.9f, 0.9f, 1f); + CustomBackground.transform.localPosition += Vector3.back * 8; + CustomBackground.gameObject.SetActive(false); + + var closeButton = Object.Instantiate(mouseMoveToggle, CustomBackground.transform); + closeButton.transform.localPosition = new(1.3f, -2.3f, -6f); + closeButton.name = "Back"; + closeButton.Text.text = Translator.GetString("Back"); + closeButton.Background.color = Palette.DisabledGrey; + var closePassiveButton = closeButton.GetComponent(); + closePassiveButton.OnClick = new(); + closePassiveButton.OnClick.AddListener(new Action(() => + { + CustomBackground.gameObject.SetActive(false); + })); + + UiElement[] selectableButtons = optionsMenuBehaviour.ControllerSelectable.ToArray(); + PassiveButton leaveButton = null; + PassiveButton returnButton = null; + for (int i = 0; i < selectableButtons.Length; i++) + { + var button = selectableButtons[i]; + if (button == null) continue; + + if (button.name == "LeaveGameButton") + leaveButton = button.GetComponent(); + else if (button.name == "ReturnToGameButton") + returnButton = button.GetComponent(); + } + var generalTab = mouseMoveToggle.transform.parent.parent.parent; + + var modOptionsButton = Object.Instantiate(mouseMoveToggle, generalTab); + modOptionsButton.transform.localPosition = leaveButton?.transform?.localPosition ?? new(0f, -2.4f, 1f); + modOptionsButton.name = "TOHEOptions"; + modOptionsButton.Text.text = Translator.GetString("TOHEOptions"); + modOptionsButton.Background.color = new Color32(255, 192, 203, byte.MaxValue); + var modOptionsPassiveButton = modOptionsButton.GetComponent(); + modOptionsPassiveButton.OnClick = new(); + modOptionsPassiveButton.OnClick.AddListener(new Action(() => + { + CustomBackground.gameObject.SetActive(true); + })); + + if (leaveButton != null) + leaveButton.transform.localPosition = new(-1.35f, -2.411f, -1f); + if (returnButton != null) + returnButton.transform.localPosition = new(1.35f, -2.411f, -1f); + } + + // ボタン生成 + ToggleButton = Object.Instantiate(mouseMoveToggle, CustomBackground.transform); + ToggleButton.transform.localPosition = new Vector3( + // 現在のオプション数を基に位置を計算 + numOptions % 2 == 0 ? -1.3f : 1.3f, + 2.2f - (0.5f * (numOptions / 2)), + -6f); + ToggleButton.name = name; + ToggleButton.Text.text = Translator.GetString(name); + var passiveButton = ToggleButton.GetComponent(); + passiveButton.OnClick = new(); + passiveButton.OnClick.AddListener(new Action(() => + { + config.Value = !config.Value; + UpdateToggle(); + additionalOnClickAction?.Invoke(); + })); + UpdateToggle(); + } + finally { numOptions++; } + } + + public static ClientOptionItem Create( + string name, + ConfigEntry config, + OptionsMenuBehaviour optionsMenuBehaviour, + Action additionalOnClickAction = null) + { + return new(name, config, optionsMenuBehaviour, additionalOnClickAction); + } + + public void UpdateToggle() + { + if (ToggleButton == null) return; + + var color = Config.Value ? new Color32(255, 192, 203, byte.MaxValue) : new Color32(77, 77, 77, byte.MaxValue); + ToggleButton.Background.color = color; + ToggleButton.Rollover?.ChangeOutColor(color); + } +} \ No newline at end of file diff --git a/Modules/Cloud.cs b/Modules/Cloud.cs new file mode 100644 index 000000000..1546b680e --- /dev/null +++ b/Modules/Cloud.cs @@ -0,0 +1,132 @@ +using AmongUs.Data; +using HarmonyLib; +using System; +using System.IO; +using System.Net.Sockets; +using System.Reflection; +using System.Text; + +namespace TOHE; + +internal class Cloud +{ + private static string IP; + //private static int LOBBY_PORT = 0; + private static int EAC_PORT = 0; + //private static Socket ClientSocket; + private static Socket EacClientSocket; + private static long LastRepotTimeStamp = 0; + public static void Init() + { + try + { + var content = GetResourcesTxt("TOHE.Resources.Config.Port.txt"); + string[] ar = content.Split('|'); + IP = ar[0]; + //LOBBY_PORT = int.Parse(ar[1]); + EAC_PORT = int.Parse(ar[2]); + } + catch (Exception e) + { + Logger.Exception(e, "Cloud Init"); + } + } + private static string GetResourcesTxt(string path) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + stream.Position = 0; + using StreamReader reader = new(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + /*public static bool ShareLobby(bool command = false) + { + try + { + if (!Options.ShareLobby.GetBool() && !command) return false; + if (!Main.newLobby || (GameData.Instance.PlayerCount < Options.ShareLobbyMinPlayer.GetInt() && !command) || !GameStates.IsLobby) return false; + if (!AmongUsClient.Instance.AmHost || !GameData.Instance || AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame) return false; + + if (IP == null || LOBBY_PORT == 0) throw new("Has no ip or port"); + + Main.newLobby = false; + string msg = $"{GameStartManager.Instance.GameRoomNameCode.text}|{Main.PluginVersion}|{GameData.Instance.PlayerCount + 1}|{TranslationController.Instance.currentLanguage.languageID}|{ServerManager.Instance.CurrentRegion.Name}|{DataManager.player.customization.name}"; + + if (msg.Length <= 60) + { + byte[] buffer = Encoding.Default.GetBytes(msg); + ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + ClientSocket.Connect(IP, LOBBY_PORT); + ClientSocket.Send(buffer); + ClientSocket.Close(); + } + + Utils.SendMessage(Translator.GetString("Message.LobbyShared"), PlayerControl.LocalPlayer.PlayerId); + + } + catch (Exception e) + { + Utils.SendMessage(Translator.GetString("Message.LobbyShareFailed"), PlayerControl.LocalPlayer.PlayerId); + Logger.Exception(e, "SentLobbyToQQ"); + throw; + } + return true; + }*/ + + private static bool connecting = false; + public static void StartConnect() + { + if (connecting || EacClientSocket != null && EacClientSocket.Connected) return; + connecting = true; + new LateTask(() => + { + if (!AmongUsClient.Instance.AmHost || !GameData.Instance || AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame) + { + connecting = false; + return; + } + try + { + if (IP == null || EAC_PORT == 0) throw new("Has no ip or port"); + LastRepotTimeStamp = Utils.GetTimeStamp(); + EacClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + EacClientSocket.Connect(IP, EAC_PORT); + Logger.Warn("已连接至TOHE服务器", "EAC Cloud"); + } + catch (Exception e) + { + connecting = false; + Logger.Exception(e, "EAC Cloud"); + throw; + } + connecting = false; + }, 3.5f, "EAC Cloud Connect"); + } + public static void StopConnect() + { + if (EacClientSocket != null && EacClientSocket.Connected) + EacClientSocket.Close(); + } + public static void SendData(string msg) + { + StartConnect(); + if (EacClientSocket == null || !EacClientSocket.Connected) + { + Logger.Warn("未连接至TOHE服务器,报告被取消", "EAC Cloud"); + return; + } + EacClientSocket.Send(Encoding.Default.GetBytes(msg)); + } + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FixedUpdate))] + class EACConnectTimeOut + { + public static void Postfix() + { + if (LastRepotTimeStamp != 0 && LastRepotTimeStamp + 8 < Utils.GetTimeStamp()) + { + LastRepotTimeStamp = 0; + StopConnect(); + Logger.Warn("超时自动断开与TOHE服务器的连接", "EAC Cloud"); + } + } + } +} \ No newline at end of file diff --git a/Modules/CustomRoleSelector.cs b/Modules/CustomRoleSelector.cs new file mode 100644 index 000000000..9f873737b --- /dev/null +++ b/Modules/CustomRoleSelector.cs @@ -0,0 +1,368 @@ +using AmongUs.GameOptions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TOHE.Modules; + +internal class CustomRoleSelector +{ + public static Dictionary RoleResult; + public static IReadOnlyList AllRoles => RoleResult.Values.ToList(); + + public static void SelectCustomRoles() + { + // 开始职业抽取 + RoleResult = new(); + var rd = IRandom.Instance; + int playerCount = Main.AllAlivePlayerControls.Count(); + int optImpNum = Main.RealOptionsData.GetInt(Int32OptionNames.NumImpostors); + int optNonNeutralKillingNum = 0; + int optNeutralKillingNum = 0; + //int optCovenNum = 0; + + if (Options.NonNeutralKillingRolesMaxPlayer.GetInt() > 0 && Options.NonNeutralKillingRolesMaxPlayer.GetInt() >= Options.NonNeutralKillingRolesMinPlayer.GetInt()) + { + optNonNeutralKillingNum = rd.Next(Options.NonNeutralKillingRolesMinPlayer.GetInt(), Options.NonNeutralKillingRolesMaxPlayer.GetInt() + 1); + } + if (Options.NeutralKillingRolesMaxPlayer.GetInt() > 0 && Options.NeutralKillingRolesMaxPlayer.GetInt() >= Options.NeutralKillingRolesMinPlayer.GetInt()) + { + optNeutralKillingNum = rd.Next(Options.NeutralKillingRolesMinPlayer.GetInt(), Options.NeutralKillingRolesMaxPlayer.GetInt() + 1); + } + // if (Options.CovenRolesMaxPlayer.GetInt() > 0 && Options.CovenRolesMaxPlayer.GetInt() >= Options.CovenRolesMinPlayer.GetInt()) + // { + // optCovenNum = rd.Next(Options.CovenRolesMinPlayer.GetInt(), Options.CovenRolesMaxPlayer.GetInt() + 1); + // } + + int readyRoleNum = 0; + int readyNonNeutralKillingNum = 0; + int readyNeutralKillingNum = 0; + // int readyCovenNum = 0; + + List rolesToAssign = new(); + List roleList = new(); + List roleOnList = new(); + + List ImpOnList = new(); + List ImpRateList = new(); + + List NonNeutralKillingOnList = new(); + List NonNeutralKillingRateList = new(); + // List CovenOnList = new(); + + List NeutralKillingOnList = new(); + List NeutralKillingRateList = new(); + // List CovenRateList = new(); + + List roleRateList = new(); + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + { + RoleResult = new(); + foreach (var pc in Main.AllAlivePlayerControls) + RoleResult.Add(pc, CustomRoles.KB_Normal); + return; + } + + foreach (var cr in Enum.GetValues(typeof(CustomRoles))) + { + CustomRoles role = (CustomRoles)Enum.Parse(typeof(CustomRoles), cr.ToString()); + if (role.IsVanilla() || role.IsAdditionRole()) continue; + if (role is CustomRoles.GM or CustomRoles.NotAssigned) continue; + for (int i = 0; i < role.GetCount(); i++) + roleList.Add(role); + } + + // 职业设置为:优先 + foreach (var role in roleList) if (role.GetMode() == 2) + { + if (role.IsImpostor()) ImpOnList.Add(role); + else if (role.IsNonNK()) NonNeutralKillingOnList.Add(role); + else if (role.IsNK()) NeutralKillingOnList.Add(role); + // else if (role.IsCoven()) CovenOnList.Add(role); + else roleOnList.Add(role); + } + // 职业设置为:启用 + foreach (var role in roleList) if (role.GetMode() == 1) + { + if (role.IsImpostor()) ImpRateList.Add(role); + else if (role.IsNonNK()) NonNeutralKillingRateList.Add(role); + else if (role.IsNK()) NeutralKillingRateList.Add(role); + // else if (role.IsCoven()) CovenRateList.Add(role); + else roleRateList.Add(role); + } + + // 抽取优先职业(内鬼) + while (ImpOnList.Count > 0) + { + var select = ImpOnList[rd.Next(0, ImpOnList.Count)]; + ImpOnList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + Logger.Info(select.ToString() + " 加入内鬼职业待选列表(优先)", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyRoleNum >= optImpNum) break; + } + // 优先职业不足以分配,开始分配启用的职业(内鬼) + if (readyRoleNum < playerCount && readyRoleNum < optImpNum) + { + while (ImpRateList.Count > 0) + { + var select = ImpRateList[rd.Next(0, ImpRateList.Count)]; + ImpRateList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + Logger.Info(select.ToString() + " 加入内鬼职业待选列表", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyRoleNum >= optImpNum) break; + } + } + + // Select NonNeutralKilling "Always" + while (NonNeutralKillingOnList.Count > 0 && optNonNeutralKillingNum > 0) + { + var select = NonNeutralKillingOnList[rd.Next(0, NonNeutralKillingOnList.Count)]; + NonNeutralKillingOnList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + readyNonNeutralKillingNum += select.GetCount(); + Logger.Info(select.ToString() + " 加入中立职业待选列表(优先)", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyNonNeutralKillingNum >= optNonNeutralKillingNum) break; + } + + // Select NonNeutralKilling "Random" + if (readyRoleNum < playerCount && readyNonNeutralKillingNum < optNonNeutralKillingNum) + { + while (NonNeutralKillingRateList.Count > 0 && optNonNeutralKillingNum > 0) + { + var select = NonNeutralKillingRateList[rd.Next(0, NonNeutralKillingRateList.Count)]; + NonNeutralKillingRateList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + readyNonNeutralKillingNum += select.GetCount(); + Logger.Info(select.ToString() + " 加入中立职业待选列表", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyNonNeutralKillingNum >= optNonNeutralKillingNum) break; + } + } + + // Select NeutralKilling "Always" + while (NeutralKillingOnList.Count > 0 && optNeutralKillingNum > 0) + { + var select = NeutralKillingOnList[rd.Next(0, NeutralKillingOnList.Count)]; + NeutralKillingOnList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + readyNeutralKillingNum += select.GetCount(); + Logger.Info(select.ToString() + " 加入中立职业待选列表(优先)", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyNeutralKillingNum >= optNeutralKillingNum) break; + } + + // Select NeutralKilling "Random" + if (readyRoleNum < playerCount && readyNeutralKillingNum < optNeutralKillingNum) + { + while (NeutralKillingRateList.Count > 0 && optNeutralKillingNum > 0) + { + var select = NeutralKillingRateList[rd.Next(0, NeutralKillingRateList.Count)]; + NeutralKillingRateList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + readyNeutralKillingNum += select.GetCount(); + Logger.Info(select.ToString() + " 加入中立职业待选列表", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyNeutralKillingNum >= optNeutralKillingNum) break; + } + } + + /*while (CovenOnList.Count > 0 && optCovenNum > 0) + { + var select = CovenOnList[rd.Next(0, CovenOnList.Count)]; + CovenOnList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + readyCovenNum += select.GetCount(); + Logger.Info(select.ToString() + " 加入中立职业待选列表(优先)", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyCovenNum >= optCovenNum) break; + }*/ + + /*if (readyRoleNum < playerCount && readyCovenNum < optCovenNum) + { + while (CovenRateList.Count > 0 && optCovenNum > 0) + { + var select = CovenRateList[rd.Next(0, CovenRateList.Count)]; + CovenRateList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + readyCovenNum += select.GetCount(); + Logger.Info(select.ToString() + " 加入中立职业待选列表", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + if (readyCovenNum >= optCovenNum) break; + } + }*/ + + // 抽取优先职业 + while (roleOnList.Count > 0) + { + var select = roleOnList[rd.Next(0, roleOnList.Count)]; + roleOnList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + Logger.Info(select.ToString() + " 加入船员职业待选列表(优先)", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + } + // 优先职业不足以分配,开始分配启用的职业 + if (readyRoleNum < playerCount) + { + while (roleRateList.Count > 0) + { + var select = roleRateList[rd.Next(0, roleRateList.Count)]; + roleRateList.Remove(select); + rolesToAssign.Add(select); + readyRoleNum++; + Logger.Info(select.ToString() + " 加入船员职业待选列表", "CustomRoleSelector"); + if (readyRoleNum >= playerCount) goto EndOfAssign; + } + } + + // 职业抽取结束 + EndOfAssign: + + // 隐藏职业 + { + if (rd.Next(0, 100) < Options.SunnyboyChance.GetInt() && rolesToAssign.Remove(CustomRoles.Jester)) rolesToAssign.Add(CustomRoles.Sunnyboy); + } + { + if (rd.Next(0, 100) < Sans.BardChance.GetInt() && rolesToAssign.Remove(CustomRoles.Sans)) rolesToAssign.Add(CustomRoles.Bard); + } + { + if (rd.Next(0, 100) < Options.NukerChance.GetInt() && rolesToAssign.Remove(CustomRoles.Bomber)) rolesToAssign.Add(CustomRoles.Nuker); + } + + { + /* if (!rolesToAssign.Contains(CustomRoles.Lovers) && rolesToAssign.Contains(CustomRoles.FFF) || !rolesToAssign.Contains(CustomRoles.Ntr) && rolesToAssign.Contains(CustomRoles.FFF)) + rolesToAssign.Remove(CustomRoles.FFF); + rolesToAssign.Add(CustomRoles.Jester); */ + + } + /* if (!Options.DisableSaboteur.GetBool()) // no longer hidden + { + if (rd.Next(0, 100) < 25 && rolesToAssign.Remove(CustomRoles.Inhibitor)) rolesToAssign.Add(CustomRoles.Saboteur); + } */ + + // EAC封禁名单玩家开房将被分配为小丑 + if (BanManager.CheckEACList(PlayerControl.LocalPlayer.FriendCode)) + { + if (!rolesToAssign.Contains(CustomRoles.Jester)) + rolesToAssign.Add(CustomRoles.Jester); + Main.DevRole.Remove(PlayerControl.LocalPlayer.PlayerId); + Main.DevRole.Add(PlayerControl.LocalPlayer.PlayerId, CustomRoles.Jester); + } + + // Dev Roles List Edit + foreach (var dr in Main.DevRole) + { + if (dr.Key == PlayerControl.LocalPlayer.PlayerId && Options.EnableGM.GetBool()) continue; + if (rolesToAssign.Contains(dr.Value)) + { + rolesToAssign.Remove(dr.Value); + rolesToAssign.Insert(0, dr.Value); + Logger.Info("职业列表提高优先:" + dr.Value, "Dev Role"); + continue; + } + for (int i = 0; i < rolesToAssign.Count; i++) + { + var role = rolesToAssign[i]; + if (dr.Value.GetMode() != role.GetMode()) continue; + if ( + (dr.Value.IsImpostor() && role.IsImpostor()) || + (dr.Value.IsNonNK() && role.IsNonNK()) || + (dr.Value.IsNK() && role.IsNK()) || + (dr.Value.IsCrewmate() & role.IsCrewmate()) + ) + { + rolesToAssign.RemoveAt(i); + rolesToAssign.Insert(0, dr.Value); + Logger.Info("覆盖职业列表:" + i + " " + role.ToString() + " => " + dr.Value, "Dev Role"); + break; + } + } + } + + var AllPlayer = Main.AllAlivePlayerControls.ToList(); + + while (AllPlayer.Count() > 0 && rolesToAssign.Count > 0) + { + PlayerControl delPc = null; + foreach (var pc in AllPlayer) + foreach (var dr in Main.DevRole.Where(x => pc.PlayerId == x.Key)) + { + if (dr.Key == PlayerControl.LocalPlayer.PlayerId && Options.EnableGM.GetBool()) continue; + var id = rolesToAssign.IndexOf(dr.Value); + if (id == -1) continue; + RoleResult.Add(pc, rolesToAssign[id]); + Logger.Info($"职业优先分配:{AllPlayer[0].GetRealName()} => {rolesToAssign[id]}", "CustomRoleSelector"); + delPc = pc; + rolesToAssign.RemoveAt(id); + goto EndOfWhile; + } + + var roleId = rd.Next(0, rolesToAssign.Count); + RoleResult.Add(AllPlayer[0], rolesToAssign[roleId]); + Logger.Info($"职业分配:{AllPlayer[0].GetRealName()} => {rolesToAssign[roleId]}", "CustomRoleSelector"); + AllPlayer.RemoveAt(0); + rolesToAssign.RemoveAt(roleId); + + EndOfWhile:; + if (delPc != null) + { + AllPlayer.Remove(delPc); + Main.DevRole.Remove(delPc.PlayerId); + } + } + + if (AllPlayer.Count() > 0) + Logger.Error("职业分配错误:存在未被分配职业的玩家", "CustomRoleSelector"); + if (rolesToAssign.Count > 0) + Logger.Error("职业分配错误:存在未被分配的职业", "CustomRoleSelector"); + + } + + public static int addScientistNum = 0; + public static int addEngineerNum = 0; + public static int addShapeshifterNum = 0; + public static void CalculateVanillaRoleCount() + { + // 计算原版特殊职业数量 + addEngineerNum = 0; + addScientistNum = 0; + addShapeshifterNum = 0; + foreach (var role in AllRoles) + { + switch (CustomRolesHelper.GetVNRole(role)) + { + case CustomRoles.Scientist: addScientistNum++; break; + case CustomRoles.Engineer: addEngineerNum++; break; + case CustomRoles.Shapeshifter: addShapeshifterNum++; break; + } + } + } + + public static List AddonRolesList = new(); + public static void SelectAddonRoles() + { + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) return; + + AddonRolesList = new(); + foreach (var cr in Enum.GetValues(typeof(CustomRoles))) + { + CustomRoles role = (CustomRoles)Enum.Parse(typeof(CustomRoles), cr.ToString()); + if (!role.IsAdditionRole()) continue; + if (role is CustomRoles.Madmate && Options.MadmateSpawnMode.GetInt() != 0) continue; + if (role is CustomRoles.Lovers or CustomRoles.LastImpostor or CustomRoles.Workhorse) continue; + AddonRolesList.Add(role); + } + } +} \ No newline at end of file diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs new file mode 100644 index 000000000..765b70d33 --- /dev/null +++ b/Modules/CustomRolesHelper.cs @@ -0,0 +1,1174 @@ +using AmongUs.GameOptions; +using System.Linq; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Neutral; + +namespace TOHE; + +internal static class CustomRolesHelper +{ + public static CustomRoles GetVNRole(this CustomRoles role) + { + return role.IsVanilla() + ? role + : role switch + { + CustomRoles.Sniper => CustomRoles.Shapeshifter, + CustomRoles.Jester => Options.JesterCanVent.GetBool() ? CustomRoles.Engineer : CustomRoles.Crewmate, + CustomRoles.Mayor => Options.MayorHasPortableButton.GetBool() ? CustomRoles.Engineer : CustomRoles.Crewmate, + CustomRoles.Vulture => Vulture.CanVent.GetBool() ? CustomRoles.Engineer : CustomRoles.Crewmate, + CustomRoles.Opportunist => CustomRoles.Crewmate, + CustomRoles.Vindicator => CustomRoles.Impostor, + CustomRoles.Snitch => CustomRoles.Crewmate, + CustomRoles.Masochist => CustomRoles.Crewmate, + CustomRoles.ParityCop => CustomRoles.Crewmate, + CustomRoles.Marshall => CustomRoles.Crewmate, + CustomRoles.SabotageMaster => CustomRoles.Engineer, + CustomRoles.Mafia => Options.LegacyMafia.GetBool() ? CustomRoles.Shapeshifter : CustomRoles.Impostor, + CustomRoles.Terrorist => CustomRoles.Engineer, + CustomRoles.Executioner => CustomRoles.Crewmate, + CustomRoles.Juggernaut => CustomRoles.Impostor, + CustomRoles.Lawyer => CustomRoles.Crewmate, + CustomRoles.Vampire => CustomRoles.Impostor, + CustomRoles.Poisoner => CustomRoles.Impostor, + CustomRoles.NSerialKiller => CustomRoles.Impostor, + CustomRoles.Maverick => CustomRoles.Impostor, + CustomRoles.CursedSoul => CustomRoles.Impostor, + CustomRoles.Parasite => CustomRoles.Impostor, + CustomRoles.BountyHunter => CustomRoles.Shapeshifter, + CustomRoles.Trickster => CustomRoles.Impostor, + CustomRoles.Witch => CustomRoles.Impostor, + CustomRoles.ShapeMaster => CustomRoles.Shapeshifter, + CustomRoles.ShapeshifterTOHE => CustomRoles.Shapeshifter, + CustomRoles.ImpostorTOHE => CustomRoles.Impostor, + CustomRoles.EvilDiviner => CustomRoles.Impostor, + CustomRoles.Ritualist => CustomRoles.Impostor, + CustomRoles.Pickpocket => CustomRoles.Impostor, + CustomRoles.Traitor => CustomRoles.Impostor, + CustomRoles.HexMaster => CustomRoles.Impostor, + CustomRoles.Wildling => CustomRoles.Shapeshifter, + CustomRoles.Morphling => CustomRoles.Shapeshifter, + CustomRoles.Warlock => CustomRoles.Shapeshifter, + CustomRoles.SerialKiller => CustomRoles.Shapeshifter, + CustomRoles.FireWorks => CustomRoles.Shapeshifter, + CustomRoles.SpeedBooster => CustomRoles.Crewmate, + CustomRoles.Dictator => CustomRoles.Crewmate, + // CustomRoles.Mare => CustomRoles.Impostor, + CustomRoles.Inhibitor => CustomRoles.Impostor, + CustomRoles.Saboteur => CustomRoles.Impostor, + CustomRoles.Doctor => CustomRoles.Scientist, + CustomRoles.ScientistTOHE => CustomRoles.Scientist, + CustomRoles.Tracefinder => CustomRoles.Scientist, + CustomRoles.Puppeteer => CustomRoles.Impostor, + CustomRoles.NWitch => CustomRoles.Impostor, + CustomRoles.TimeThief => CustomRoles.Impostor, + CustomRoles.EvilTracker => CustomRoles.Shapeshifter, + CustomRoles.Paranoia => CustomRoles.Engineer, + CustomRoles.EngineerTOHE => CustomRoles.Engineer, + CustomRoles.TimeMaster => CustomRoles.Engineer, + CustomRoles.CrewmateTOHE => CustomRoles.Crewmate, + CustomRoles.Miner => CustomRoles.Shapeshifter, + CustomRoles.Psychic => CustomRoles.Crewmate, + CustomRoles.Needy => CustomRoles.Crewmate, + CustomRoles.Twister => CustomRoles.Shapeshifter, + CustomRoles.SuperStar => CustomRoles.Crewmate, + CustomRoles.Hacker => CustomRoles.Shapeshifter, + CustomRoles.Visionary => CustomRoles.Impostor, + CustomRoles.Assassin => CustomRoles.Shapeshifter, + CustomRoles.Luckey => CustomRoles.Crewmate, + CustomRoles.CyberStar => CustomRoles.Crewmate, + CustomRoles.Escapee => CustomRoles.Shapeshifter, + CustomRoles.NiceGuesser => CustomRoles.Crewmate, + CustomRoles.EvilGuesser => CustomRoles.Impostor, + CustomRoles.Detective => CustomRoles.Crewmate, + // CustomRoles.Minimalism => CustomRoles.Impostor, + CustomRoles.God => CustomRoles.Crewmate, + CustomRoles.GuardianAngelTOHE => CustomRoles.GuardianAngel, + CustomRoles.Zombie => CustomRoles.Impostor, + CustomRoles.Mario => CustomRoles.Engineer, + CustomRoles.AntiAdminer => CustomRoles.Impostor, + CustomRoles.Sans => CustomRoles.Impostor, + CustomRoles.Bomber => CustomRoles.Shapeshifter, + CustomRoles.Nuker => CustomRoles.Shapeshifter, + // CustomRoles.Flashbang => CustomRoles.Shapeshifter, + CustomRoles.BoobyTrap => CustomRoles.Impostor, + CustomRoles.Scavenger => CustomRoles.Impostor, + CustomRoles.Transporter => CustomRoles.Crewmate, + CustomRoles.Veteran => CustomRoles.Engineer, + CustomRoles.Capitalism => CustomRoles.Impostor, + CustomRoles.Bodyguard => CustomRoles.Crewmate, + CustomRoles.Grenadier => CustomRoles.Engineer, + CustomRoles.Gangster => CustomRoles.Impostor, + CustomRoles.Cleaner => CustomRoles.Impostor, + CustomRoles.Medusa => CustomRoles.Impostor, + CustomRoles.Konan => CustomRoles.Crewmate, + CustomRoles.Divinator => CustomRoles.Crewmate, + CustomRoles.Oracle => CustomRoles.Crewmate, + CustomRoles.BallLightning => CustomRoles.Impostor, + CustomRoles.Greedier => CustomRoles.Impostor, + CustomRoles.Workaholic => CustomRoles.Engineer, + CustomRoles.CursedWolf => CustomRoles.Impostor, + CustomRoles.Jinx => CustomRoles.Impostor, + CustomRoles.Collector => CustomRoles.Crewmate, + CustomRoles.Glitch => CustomRoles.Impostor, + CustomRoles.ImperiusCurse => CustomRoles.Shapeshifter, + CustomRoles.QuickShooter => CustomRoles.Shapeshifter, + CustomRoles.Eraser => CustomRoles.Impostor, + CustomRoles.OverKiller => CustomRoles.Impostor, + CustomRoles.Hangman => CustomRoles.Shapeshifter, + CustomRoles.Sunnyboy => CustomRoles.Scientist, + CustomRoles.Phantom => Options.PhantomCanVent.GetBool() ? CustomRoles.Engineer : CustomRoles.Crewmate, + CustomRoles.Judge => CustomRoles.Crewmate, + CustomRoles.Councillor => CustomRoles.Impostor, + CustomRoles.Mortician => CustomRoles.Crewmate, + CustomRoles.Mediumshiper => CustomRoles.Crewmate, + CustomRoles.Bard => CustomRoles.Impostor, + CustomRoles.Swooper => CustomRoles.Impostor, + CustomRoles.Wraith => CustomRoles.Impostor, + CustomRoles.Crewpostor => CustomRoles.Engineer, + CustomRoles.Observer => CustomRoles.Crewmate, + CustomRoles.DovesOfNeace => CustomRoles.Engineer, + CustomRoles.Infectious => CustomRoles.Impostor, + CustomRoles.Virus => CustomRoles.Virus, + CustomRoles.Disperser => CustomRoles.Shapeshifter, + CustomRoles.Camouflager => CustomRoles.Shapeshifter, + CustomRoles.Dazzler => CustomRoles.Shapeshifter, + CustomRoles.Devourer => CustomRoles.Shapeshifter, + CustomRoles.Deathpact => CustomRoles.Shapeshifter, + // CustomRoles.Monarch => CustomRoles.Impostor, + CustomRoles.Bloodhound => CustomRoles.Crewmate, + CustomRoles.Tracker => CustomRoles.Crewmate, + CustomRoles.Merchant => CustomRoles.Crewmate, + CustomRoles.Retributionist => CustomRoles.Crewmate, + CustomRoles.Guardian => CustomRoles.Crewmate, + CustomRoles.Addict => CustomRoles.Engineer, + CustomRoles.Chameleon => CustomRoles.Engineer, + CustomRoles.Spiritcaller => CustomRoles.Impostor, + CustomRoles.EvilSpirit => CustomRoles.GuardianAngel, + CustomRoles.Lurker => CustomRoles.Impostor, + CustomRoles.PlagueBearer => CustomRoles.Impostor, + CustomRoles.Pestilence => CustomRoles.Impostor, + CustomRoles.Doomsayer => CustomRoles.Crewmate, + CustomRoles.Godfather => CustomRoles.Impostor, + + _ => role.IsImpostor() ? CustomRoles.Impostor : CustomRoles.Crewmate, + }; + } + + public static CustomRoles GetErasedRole(this CustomRoles role) + { + return role.IsVanilla() + ? role + : role switch + { + CustomRoles.Tracker => CustomRoles.CrewmateTOHE, + CustomRoles.Mayor => Options.MayorHasPortableButton.GetBool() ? CustomRoles.EngineerTOHE : CustomRoles.CrewmateTOHE, + CustomRoles.Observer => CustomRoles.CrewmateTOHE, + CustomRoles.DovesOfNeace => CustomRoles.EngineerTOHE, + CustomRoles.Judge => CustomRoles.CrewmateTOHE, + CustomRoles.Mortician => CustomRoles.CrewmateTOHE, + CustomRoles.Mediumshiper => CustomRoles.CrewmateTOHE, + // CustomRoles.Glitch => CustomRoles.CrewmateTOHE, + CustomRoles.Bodyguard => CustomRoles.CrewmateTOHE, + CustomRoles.ParityCop => CustomRoles.CrewmateTOHE, + CustomRoles.Grenadier => CustomRoles.EngineerTOHE, + CustomRoles.Transporter => CustomRoles.CrewmateTOHE, + CustomRoles.Veteran => CustomRoles.CrewmateTOHE, + CustomRoles.GuardianAngelTOHE => CustomRoles.GuardianAngel, + CustomRoles.Detective => CustomRoles.CrewmateTOHE, + CustomRoles.NiceGuesser => CustomRoles.CrewmateTOHE, + CustomRoles.Luckey => CustomRoles.CrewmateTOHE, + CustomRoles.CyberStar => CustomRoles.CrewmateTOHE, + CustomRoles.Psychic => CustomRoles.CrewmateTOHE, + CustomRoles.Needy => CustomRoles.CrewmateTOHE, + CustomRoles.SuperStar => CustomRoles.CrewmateTOHE, + CustomRoles.Paranoia => CustomRoles.EngineerTOHE, + CustomRoles.EngineerTOHE => CustomRoles.EngineerTOHE, + CustomRoles.TimeMaster => CustomRoles.EngineerTOHE, + CustomRoles.Reverie => CustomRoles.CrewmateTOHE, + CustomRoles.CrewmateTOHE => CustomRoles.CrewmateTOHE, + CustomRoles.Doctor => CustomRoles.ScientistTOHE, + CustomRoles.ScientistTOHE => CustomRoles.ScientistTOHE, + CustomRoles.Tracefinder => CustomRoles.ScientistTOHE, + CustomRoles.SpeedBooster => CustomRoles.CrewmateTOHE, + CustomRoles.Dictator => CustomRoles.CrewmateTOHE, + CustomRoles.Snitch => CustomRoles.CrewmateTOHE, + CustomRoles.Marshall => CustomRoles.CrewmateTOHE, + CustomRoles.SabotageMaster => CustomRoles.EngineerTOHE, + CustomRoles.Retributionist => CustomRoles.Crewmate, + CustomRoles.Monarch => CustomRoles.CrewmateTOHE, + CustomRoles.Deputy => CustomRoles.CrewmateTOHE, + CustomRoles.Guardian => CustomRoles.CrewmateTOHE, + CustomRoles.Addict => CustomRoles.EngineerTOHE, + CustomRoles.Oracle => CustomRoles.EngineerTOHE, + CustomRoles.Chameleon => CustomRoles.EngineerTOHE, + _ => role.IsImpostor() ? CustomRoles.ImpostorTOHE : CustomRoles.CrewmateTOHE, + }; + } + + public static RoleTypes GetDYRole(this CustomRoles role) + { + return role switch + { + //SoloKombat + CustomRoles.KB_Normal => RoleTypes.Impostor, + //Standard + CustomRoles.Sheriff => RoleTypes.Impostor, + CustomRoles.Crusader => RoleTypes.Impostor, + CustomRoles.Pirate => RoleTypes.Impostor, + CustomRoles.CopyCat => RoleTypes.Impostor, + CustomRoles.CursedSoul => RoleTypes.Impostor, + CustomRoles.Admirer => RoleTypes.Impostor, + CustomRoles.Refugee => RoleTypes.Impostor, + CustomRoles.Amnesiac => RoleTypes.Impostor, + CustomRoles.Monarch => RoleTypes.Impostor, + CustomRoles.Deputy => RoleTypes.Impostor, + CustomRoles.Arsonist => RoleTypes.Impostor, + CustomRoles.Jackal => RoleTypes.Impostor, + CustomRoles.Medusa => RoleTypes.Impostor, + CustomRoles.Sidekick => RoleTypes.Impostor, + CustomRoles.SwordsMan => RoleTypes.Impostor, + CustomRoles.Reverie => RoleTypes.Impostor, + CustomRoles.Innocent => RoleTypes.Impostor, + CustomRoles.Pelican => RoleTypes.Impostor, + CustomRoles.Counterfeiter => RoleTypes.Impostor, + CustomRoles.Pursuer => RoleTypes.Impostor, + CustomRoles.Revolutionist => RoleTypes.Impostor, + CustomRoles.FFF => RoleTypes.Impostor, + CustomRoles.Medic => RoleTypes.Impostor, + CustomRoles.Gamer => RoleTypes.Impostor, + CustomRoles.HexMaster => RoleTypes.Impostor, + CustomRoles.Wraith => RoleTypes.Impostor, + CustomRoles.Glitch => RoleTypes.Impostor, + CustomRoles.Juggernaut => RoleTypes.Impostor, + CustomRoles.Jinx => RoleTypes.Impostor, + CustomRoles.DarkHide => RoleTypes.Impostor, + CustomRoles.Provocateur => RoleTypes.Impostor, + CustomRoles.BloodKnight => RoleTypes.Impostor, + CustomRoles.Poisoner => RoleTypes.Impostor, + CustomRoles.NSerialKiller => RoleTypes.Impostor, + CustomRoles.Maverick => RoleTypes.Impostor, + CustomRoles.Parasite => RoleTypes.Impostor, + CustomRoles.NWitch => RoleTypes.Impostor, + CustomRoles.Totocalcio => RoleTypes.Impostor, + CustomRoles.Succubus => RoleTypes.Impostor, + CustomRoles.Infectious => RoleTypes.Impostor, + CustomRoles.Virus => RoleTypes.Impostor, + CustomRoles.Farseer => RoleTypes.Impostor, + CustomRoles.Ritualist => RoleTypes.Impostor, + CustomRoles.Pickpocket => RoleTypes.Impostor, + CustomRoles.Traitor => RoleTypes.Impostor, + CustomRoles.PlagueBearer => RoleTypes.Impostor, + CustomRoles.Pestilence => RoleTypes.Impostor, + CustomRoles.Spiritcaller => RoleTypes.Impostor, + _ => RoleTypes.GuardianAngel + }; + } + public static bool IsAdditionRole(this CustomRoles role) + { + return role is + CustomRoles.Lovers or + CustomRoles.LastImpostor or + CustomRoles.Ntr or + CustomRoles.Madmate or + CustomRoles.Watcher or + CustomRoles.Admired or + CustomRoles.Flashman or + CustomRoles.Lighter or + CustomRoles.Seer or + CustomRoles.Bait or + CustomRoles.Diseased or + CustomRoles.Antidote or + CustomRoles.Swift or + CustomRoles.Gravestone or + CustomRoles.Trapper or + CustomRoles.Mare or + CustomRoles.Brakar or + CustomRoles.Oblivious or + CustomRoles.Bewilder or + CustomRoles.Sunglasses or + CustomRoles.Knighted or + CustomRoles.Workhorse or + CustomRoles.Fool or + CustomRoles.Autopsy or + CustomRoles.Necroview or + CustomRoles.Avanger or + CustomRoles.Youtuber or + CustomRoles.Soulless or + CustomRoles.Loyal or + CustomRoles.Egoist or + CustomRoles.Recruit or + CustomRoles.Glow or + CustomRoles.TicketsStealer or + CustomRoles.DualPersonality or + CustomRoles.Mimic or + CustomRoles.Reach or + CustomRoles.Charmed or + CustomRoles.Infected or + CustomRoles.Onbound or + CustomRoles.Lazy or + // CustomRoles.Reflective or + CustomRoles.Rascal or + CustomRoles.Contagious or + CustomRoles.Guesser or + CustomRoles.Rogue or + CustomRoles.Unreportable or + CustomRoles.Lucky or + CustomRoles.Unlucky or + CustomRoles.DoubleShot or + CustomRoles.Ghoul or + CustomRoles.EvilSpirit; + } + public static bool IsNonNK(this CustomRoles role) // ROLE ASSIGNING, NOT NEUTRAL TYPE + { + return role is + CustomRoles.Jester or + CustomRoles.Terrorist or + CustomRoles.Opportunist or + CustomRoles.Masochist or + CustomRoles.Executioner or + CustomRoles.Mario or + CustomRoles.Crewpostor or + CustomRoles.Lawyer or + CustomRoles.God or + CustomRoles.Amnesiac or + CustomRoles.Innocent or + CustomRoles.Vulture or + CustomRoles.NWitch or + CustomRoles.Pursuer or + CustomRoles.Revolutionist or + CustomRoles.Provocateur or + CustomRoles.Gamer or + CustomRoles.FFF or + CustomRoles.Workaholic or + // CustomRoles.Pelican or + CustomRoles.Collector or + CustomRoles.Sunnyboy or + CustomRoles.Maverick or + CustomRoles.CursedSoul or + CustomRoles.Phantom or + CustomRoles.Totocalcio or + CustomRoles.Doomsayer or + CustomRoles.Succubus; + } + + public static bool IsAmneMaverick(this CustomRoles role) // ROLE ASSIGNING, NOT NEUTRAL TYPE + { + return role is + CustomRoles.Jester or + CustomRoles.Terrorist or + CustomRoles.Opportunist or + CustomRoles.Masochist or + CustomRoles.Executioner or + CustomRoles.Mario or + CustomRoles.Crewpostor or + CustomRoles.Lawyer or + CustomRoles.God or + CustomRoles.Amnesiac or + CustomRoles.Pestilence or + CustomRoles.PlagueBearer or + CustomRoles.Innocent or + CustomRoles.Vulture or + CustomRoles.NWitch or + CustomRoles.Pursuer or + CustomRoles.Revolutionist or + CustomRoles.Provocateur or + CustomRoles.Gamer or + CustomRoles.FFF or + CustomRoles.Workaholic or + // CustomRoles.Pelican or + CustomRoles.Collector or + CustomRoles.Sunnyboy or + CustomRoles.Arsonist or + CustomRoles.Maverick or + CustomRoles.CursedSoul or + CustomRoles.Phantom or + CustomRoles.DarkHide or + CustomRoles.Ritualist or + CustomRoles.Doomsayer or + CustomRoles.Pirate or + // CustomRoles.Juggernaut or + CustomRoles.Jinx or + // CustomRoles.Poisoner or + // CustomRoles.HexMaster or + CustomRoles.Totocalcio; + } + public static bool IsAmneNK(this CustomRoles role) + { + return role is + CustomRoles.Sidekick or + CustomRoles.Infectious or + CustomRoles.Glitch or + CustomRoles.Wraith or + CustomRoles.Medusa or + CustomRoles.Pelican or + CustomRoles.Refugee or + CustomRoles.Parasite or + CustomRoles.NSerialKiller or + CustomRoles.Pickpocket or + CustomRoles.Traitor or + CustomRoles.Virus or + CustomRoles.Spiritcaller or + CustomRoles.Succubus; + } + + public static bool IsNK(this CustomRoles role) + { + return role is + CustomRoles.Jackal or + CustomRoles.Glitch or + CustomRoles.Sidekick or + CustomRoles.HexMaster or + CustomRoles.Infectious or + CustomRoles.Wraith or + CustomRoles.Medusa or + CustomRoles.Pelican or + CustomRoles.Pirate or + CustomRoles.DarkHide or + CustomRoles.Juggernaut or + CustomRoles.Jinx or + CustomRoles.Poisoner or + CustomRoles.Refugee or + CustomRoles.Parasite or + CustomRoles.NSerialKiller or + CustomRoles.Ritualist or + CustomRoles.Arsonist or + CustomRoles.Pickpocket or + CustomRoles.Traitor or + CustomRoles.Virus or + CustomRoles.BloodKnight or + CustomRoles.Spiritcaller or + CustomRoles.PlagueBearer or + CustomRoles.Pestilence; + } + public static bool IsSnitchTarget(this CustomRoles role) + { + return role is + CustomRoles.Jackal or + CustomRoles.Sidekick or + CustomRoles.HexMaster or + CustomRoles.Refugee or + CustomRoles.Infectious or + CustomRoles.Wraith or + CustomRoles.Crewpostor or + CustomRoles.Juggernaut or + CustomRoles.Jinx or + CustomRoles.DarkHide or + CustomRoles.Poisoner or + CustomRoles.Parasite or + CustomRoles.NSerialKiller or + CustomRoles.Ritualist or + CustomRoles.Pickpocket or + CustomRoles.Traitor or + CustomRoles.Medusa or + CustomRoles.Gamer or + CustomRoles.Pelican or + CustomRoles.Virus or + CustomRoles.Succubus or + CustomRoles.BloodKnight or + CustomRoles.Spiritcaller or + CustomRoles.PlagueBearer or + CustomRoles.Pestilence; + } + public static bool IsNE(this CustomRoles role) + { + return role is + CustomRoles.Jester or + CustomRoles.Gamer or + CustomRoles.Masochist or + CustomRoles.Arsonist or + CustomRoles.Executioner or + CustomRoles.Doomsayer or + CustomRoles.Innocent; + } + public static bool IsNB(this CustomRoles role) + { + return role is + CustomRoles.Opportunist or + CustomRoles.Lawyer or + CustomRoles.Amnesiac or + CustomRoles.NWitch or + CustomRoles.God or + CustomRoles.Pursuer or + CustomRoles.FFF or + CustomRoles.Sunnyboy or + CustomRoles.Maverick or + CustomRoles.Totocalcio; + } + public static bool IsNC(this CustomRoles role) + { + return role is + CustomRoles.Mario or + CustomRoles.Terrorist or + CustomRoles.Pirate or + CustomRoles.Revolutionist or + CustomRoles.Vulture or + CustomRoles.Famine or + CustomRoles.Baker or + CustomRoles.CursedSoul or + CustomRoles.Phantom or + CustomRoles.Workaholic or + CustomRoles.Collector or + CustomRoles.Provocateur; + } + public static bool IsNeutralKilling(this CustomRoles role) + { + return role is + CustomRoles.Terrorist or + CustomRoles.Arsonist or + CustomRoles.Juggernaut or + CustomRoles.Medusa or + CustomRoles.Jinx or + CustomRoles.Jackal or + CustomRoles.Sidekick or + CustomRoles.Refugee or + CustomRoles.God or + CustomRoles.Mario or + CustomRoles.Innocent or + CustomRoles.Pelican or + CustomRoles.Wraith or + CustomRoles.HexMaster or + CustomRoles.Egoist or + CustomRoles.Gamer or + CustomRoles.Parasite or + CustomRoles.DarkHide or + CustomRoles.Workaholic or + CustomRoles.Collector or + CustomRoles.Traitor or + CustomRoles.Poisoner or + CustomRoles.NSerialKiller or + CustomRoles.Ritualist or + CustomRoles.Pickpocket or + CustomRoles.Traitor or + CustomRoles.BloodKnight or + CustomRoles.Infectious or + CustomRoles.Virus or + CustomRoles.Succubus or + CustomRoles.Spiritcaller or + CustomRoles.PlagueBearer or + CustomRoles.Pestilence; + } + public static bool IsCK(this CustomRoles role) + { + return role is + CustomRoles.SwordsMan or + CustomRoles.Veteran or + CustomRoles.CopyCat or + CustomRoles.Bodyguard or + CustomRoles.Reverie or + CustomRoles.Crusader or + CustomRoles.NiceGuesser or + CustomRoles.Counterfeiter or + CustomRoles.Retributionist or + CustomRoles.Sheriff; + } + public static bool IsImpostor(this CustomRoles role) // IsImp + { + return role is + CustomRoles.Impostor or + CustomRoles.Godfather or + CustomRoles.Shapeshifter or + CustomRoles.ShapeshifterTOHE or + CustomRoles.ImpostorTOHE or + CustomRoles.EvilDiviner or + CustomRoles.Wildling or + CustomRoles.Morphling or + CustomRoles.BountyHunter or + CustomRoles.Vampire or + CustomRoles.Witch or + CustomRoles.Vindicator or + CustomRoles.ShapeMaster or + CustomRoles.Zombie or + CustomRoles.Warlock or + CustomRoles.Assassin or + CustomRoles.Hacker or + CustomRoles.Visionary or + CustomRoles.Miner or + CustomRoles.Escapee or + CustomRoles.SerialKiller or + CustomRoles.Underdog or + CustomRoles.Inhibitor or + CustomRoles.Councillor or + CustomRoles.Saboteur or + CustomRoles.Puppeteer or + CustomRoles.TimeThief or + CustomRoles.Trickster or + CustomRoles.Mafia or + CustomRoles.Minimalism or + CustomRoles.FireWorks or + CustomRoles.Sniper or + CustomRoles.EvilTracker or + CustomRoles.EvilGuesser or + CustomRoles.AntiAdminer or + CustomRoles.Sans or + CustomRoles.Bomber or + CustomRoles.Nuker or + CustomRoles.Scavenger or + CustomRoles.BoobyTrap or + CustomRoles.Capitalism or + CustomRoles.Gangster or + CustomRoles.Cleaner or + CustomRoles.BallLightning or + CustomRoles.Greedier or + CustomRoles.CursedWolf or + CustomRoles.ImperiusCurse or + CustomRoles.QuickShooter or + CustomRoles.Eraser or + CustomRoles.OverKiller or + CustomRoles.Hangman or + CustomRoles.Bard or + CustomRoles.Swooper or + CustomRoles.Disperser or + CustomRoles.Dazzler or + CustomRoles.Deathpact or + CustomRoles.Devourer or + CustomRoles.Camouflager or + CustomRoles.Twister or + CustomRoles.Lurker; + } + public static bool IsNeutral(this CustomRoles role) + { + return role is + //SoloKombat + CustomRoles.KB_Normal or + //Standard + CustomRoles.Jester or + CustomRoles.Opportunist or + CustomRoles.Mario or + CustomRoles.Masochist or + CustomRoles.Amnesiac or + CustomRoles.Medusa or + CustomRoles.Famine or + CustomRoles.Baker or + CustomRoles.HexMaster or + CustomRoles.Glitch or + CustomRoles.Crewpostor or + CustomRoles.NWitch or + CustomRoles.Wraith or + CustomRoles.Vulture or + CustomRoles.Parasite or + CustomRoles.Terrorist or + CustomRoles.Executioner or + CustomRoles.Juggernaut or + CustomRoles.Refugee or + CustomRoles.Jinx or + CustomRoles.Lawyer or + CustomRoles.Arsonist or + CustomRoles.Sidekick or + CustomRoles.Jackal or + CustomRoles.God or + CustomRoles.Innocent or + CustomRoles.Pursuer or + CustomRoles.PlagueBearer or + CustomRoles.Pestilence or + CustomRoles.Pirate or + + // CustomRoles.Sidekick or + CustomRoles.Poisoner or + CustomRoles.NSerialKiller or + CustomRoles.Ritualist or + CustomRoles.Pickpocket or + CustomRoles.Pelican or + CustomRoles.Traitor or + CustomRoles.Revolutionist or + CustomRoles.FFF or + CustomRoles.Konan or + CustomRoles.Gamer or + CustomRoles.Maverick or + CustomRoles.CursedSoul or + CustomRoles.DarkHide or + CustomRoles.Infectious or + CustomRoles.Workaholic or + CustomRoles.Collector or + CustomRoles.Provocateur or + CustomRoles.Sunnyboy or + CustomRoles.Phantom or + CustomRoles.BloodKnight or + CustomRoles.Totocalcio or + CustomRoles.Virus or + CustomRoles.Succubus or + CustomRoles.Doomsayer or + CustomRoles.Spiritcaller; + } + + public static bool IsAbleToBeSidekicked(this CustomRoles role) + { + return role is + CustomRoles.BloodKnight or + CustomRoles.Virus or + CustomRoles.Medusa or + CustomRoles.NSerialKiller or + CustomRoles.Traitor or + CustomRoles.HexMaster or + CustomRoles.Sheriff or + CustomRoles.Medic or + CustomRoles.Crusader or + CustomRoles.Deputy or + CustomRoles.Glitch or + CustomRoles.Ritualist or + CustomRoles.CopyCat or + CustomRoles.Pickpocket or + CustomRoles.Poisoner or + CustomRoles.Reverie or + CustomRoles.Arsonist or + CustomRoles.Revolutionist or + CustomRoles.Maverick or + CustomRoles.NWitch or + CustomRoles.Succubus or + CustomRoles.Gamer or + CustomRoles.DarkHide or + CustomRoles.Provocateur or + CustomRoles.Wraith or + CustomRoles.Juggernaut or + CustomRoles.Pelican or + CustomRoles.Infectious or + CustomRoles.Pursuer or + CustomRoles.Jinx or + CustomRoles.Counterfeiter or + CustomRoles.Totocalcio or + CustomRoles.Farseer or + CustomRoles.FFF or + CustomRoles.SwordsMan or + CustomRoles.CursedSoul or + CustomRoles.Admirer or + CustomRoles.Refugee or + CustomRoles.Amnesiac or + CustomRoles.Monarch or + CustomRoles.Parasite or + CustomRoles.PlagueBearer; + + } + + public static bool IsNeutralWithGuessAccess(this CustomRoles role) + { + return role is + //SoloKombat + CustomRoles.KB_Normal or + //Standard + CustomRoles.Jester or + CustomRoles.Opportunist or + CustomRoles.Mario or + CustomRoles.HexMaster or + CustomRoles.Crewpostor or + CustomRoles.NWitch or + CustomRoles.Wraith or + CustomRoles.Parasite or + CustomRoles.Terrorist or + CustomRoles.Executioner or + CustomRoles.Medusa or + CustomRoles.Juggernaut or + CustomRoles.Vulture or + CustomRoles.Jinx or + CustomRoles.Lawyer or + CustomRoles.Arsonist or + CustomRoles.Jackal or + CustomRoles.Sidekick or + CustomRoles.God or + CustomRoles.Innocent or + CustomRoles.Pursuer or + // CustomRoles.Sidekick or + CustomRoles.Poisoner or + CustomRoles.NSerialKiller or + CustomRoles.Pelican or + CustomRoles.Revolutionist or + CustomRoles.FFF or + CustomRoles.Traitor or + CustomRoles.Konan or + CustomRoles.Gamer or + CustomRoles.DarkHide or + CustomRoles.Infectious or + CustomRoles.Workaholic or + CustomRoles.Collector or + CustomRoles.Provocateur or + CustomRoles.Sunnyboy or + CustomRoles.Phantom or + CustomRoles.BloodKnight or + CustomRoles.Totocalcio or + CustomRoles.Virus or + CustomRoles.Succubus or + CustomRoles.Spiritcaller or + CustomRoles.Doomsayer or + CustomRoles.PlagueBearer or + CustomRoles.Pirate or + CustomRoles.Pestilence; + } + + public static bool IsEvilAddons(this CustomRoles role) + { + return role is + CustomRoles.Madmate or + CustomRoles.Egoist or + CustomRoles.Charmed or + CustomRoles.Recruit or + CustomRoles.Infected or + CustomRoles.Contagious or + CustomRoles.Rogue or + CustomRoles.Rascal or + CustomRoles.Soulless; + } + + public static bool IsMadmate(this CustomRoles role) + { + return role is + CustomRoles.Crewpostor or + CustomRoles.Convict or + CustomRoles.Refugee or + CustomRoles.Parasite; + } + public static bool IsTasklessCrewmate(this CustomRoles role) + { + return role is + CustomRoles.Sheriff or + CustomRoles.Medic or + CustomRoles.CopyCat or + CustomRoles.Reverie or + CustomRoles.Crusader or + CustomRoles.Counterfeiter or + CustomRoles.Monarch or + CustomRoles.Farseer or + CustomRoles.SwordsMan or + CustomRoles.Deputy; + } + public static bool IsTaskBasedCrewmate(this CustomRoles role) + { + return role is + CustomRoles.Snitch or + CustomRoles.Divinator or + CustomRoles.Marshall or + CustomRoles.TimeManager or + CustomRoles.Guardian or + CustomRoles.Merchant or + CustomRoles.Mayor or + CustomRoles.Transporter; + } + + public static bool IsNotKnightable(this CustomRoles role) + { + return role is + CustomRoles.Mayor or + CustomRoles.Vindicator or + CustomRoles.Dictator or + CustomRoles.Knighted or + CustomRoles.Glitch or + CustomRoles.Pickpocket or + CustomRoles.TicketsStealer; + } + public static bool CheckAddonConfilct(CustomRoles role, PlayerControl pc) + { + if (!role.IsAdditionRole()) return false; + + if (pc.Is(CustomRoles.GM) || pc.Is(CustomRoles.Lovers) || (pc.HasSubRole() && pc.GetCustomSubRoles().Count >= Options.NoLimitAddonsNumMax.GetInt())) return false; + if (role is CustomRoles.Lighter && (!pc.GetCustomRole().IsCrewmate() || pc.Is(CustomRoles.Bewilder) || pc.Is(CustomRoles.Sunglasses) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + if (role is CustomRoles.Bewilder && (pc.Is(CustomRoles.Lighter) || pc.Is(CustomRoles.GuardianAngelTOHE) || pc.Is(CustomRoles.Sunglasses))) return false; + if (role is CustomRoles.Sunglasses && (pc.Is(CustomRoles.Lighter) || pc.Is(CustomRoles.GuardianAngelTOHE) || pc.Is(CustomRoles.Bewilder))) return false; + if (role is CustomRoles.Ntr && (pc.Is(CustomRoles.Lovers) || pc.Is(CustomRoles.FFF) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + // if (role is CustomRoles.Lovers && pc.Is(CustomRoles.FFF)) return false; + // if (role is CustomRoles.Lovers && pc.Is(CustomRoles.Bomber)) return false; + if (role is CustomRoles.Guesser && ((pc.GetCustomRole().IsCrewmate() && (!Options.CrewCanBeGuesser.GetBool() || pc.Is(CustomRoles.NiceGuesser) || pc.Is(CustomRoles.Judge))) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeGuesser.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeGuesser.GetBool() || pc.Is(CustomRoles.EvilGuesser)) || pc.Is(CustomRoles.Mafia) || pc.Is(CustomRoles.Councillor) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + if (role is CustomRoles.Madmate && (!Utils.CanBeMadmate(pc))) return false; + if (role is CustomRoles.Oblivious && (pc.Is(CustomRoles.Detective) || pc.Is(CustomRoles.Cleaner) || pc.Is(CustomRoles.Medusa) || pc.Is(CustomRoles.Mortician) || pc.Is(CustomRoles.Mediumshiper) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + if (role is CustomRoles.Avanger && pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeAvanger.GetBool() || pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Brakar && pc.Is(CustomRoles.Dictator) || pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Youtuber && (!pc.GetCustomRole().IsCrewmate() || pc.Is(CustomRoles.Madmate) || pc.Is(CustomRoles.Sheriff) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + if (role is CustomRoles.Egoist && (pc.GetCustomRole().IsNeutral() || pc.Is(CustomRoles.Madmate))) return false; + if (role is CustomRoles.Lazy && (pc.GetCustomRole().IsNeutral() || pc.GetCustomRole().IsImpostor() || pc.GetCustomRole().IsTasklessCrewmate() && !Options.TasklessCrewCanBeLazy.GetBool() || pc.GetCustomRole().IsTaskBasedCrewmate() && !Options.TaskBasedCrewCanBeLazy.GetBool()) || pc.Is(CustomRoles.Needy) || pc.Is(CustomRoles.Snitch) || pc.Is(CustomRoles.Marshall) || pc.Is(CustomRoles.Transporter) || pc.Is(CustomRoles.Guardian)) return false; + if (role is CustomRoles.Ghoul && (pc.GetCustomRole().IsNeutral() || pc.GetCustomRole().IsImpostor() || pc.GetCustomRole().IsTasklessCrewmate() || pc.GetCustomRole().IsTaskBasedCrewmate()) || pc.Is(CustomRoles.Needy) || pc.Is(CustomRoles.Snitch) || pc.Is(CustomRoles.Marshall) || pc.Is(CustomRoles.Transporter) || pc.Is(CustomRoles.Guardian)) return false; + if (role is CustomRoles.Egoist && pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeEgoist.GetBool()) return false; + if (role is CustomRoles.Egoist && pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeEgoist.GetBool() || pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.TicketsStealer or CustomRoles.Mimic or CustomRoles.Swift or CustomRoles.Mare && !pc.GetCustomRole().IsImpostor()) return false; + if (role is CustomRoles.TicketsStealer or CustomRoles.Swift or CustomRoles.Mare && (pc.Is(CustomRoles.Bomber) || pc.Is(CustomRoles.Nuker) || pc.Is(CustomRoles.BoobyTrap) || pc.Is(CustomRoles.Capitalism))) return false; + if (role is CustomRoles.Necroview && pc.Is(CustomRoles.Visionary)) return false; + if (role is CustomRoles.Ghoul && pc.Is(CustomRoles.Lazy)) return false; + if (role is CustomRoles.Lazy && pc.Is(CustomRoles.Ghoul)) return false; + if (role is CustomRoles.Ghoul && pc.Is(CustomRoles.Needy)) return false; + if (role is CustomRoles.Mimic && pc.Is(CustomRoles.Mafia)) return false; + if (role is CustomRoles.Rascal && !pc.GetCustomRole().IsCrewmate()) return false; + if (role is CustomRoles.Needy && pc.GetCustomRole().IsAdditionRole()) return false; + if (role is CustomRoles.TicketsStealer && pc.Is(CustomRoles.Vindicator)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Underdog)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Inhibitor)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Saboteur)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Swift)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Mafia)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Sniper)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.FireWorks)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Swooper)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Vampire)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.Sans)) return false; + if (role is CustomRoles.Mare && pc.Is(CustomRoles.LastImpostor)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Mare)) return false; + if (role is CustomRoles.DualPersonality && ((!pc.GetCustomRole().IsImpostor() && !pc.GetCustomRole().IsCrewmate()) || pc.Is(CustomRoles.Madmate))) return false; + if (role is CustomRoles.DualPersonality && pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeDualPersonality.GetBool()) return false; + if (role is CustomRoles.DualPersonality && pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeDualPersonality.GetBool() || pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Loyal && ((!pc.GetCustomRole().IsImpostor() && !pc.GetCustomRole().IsCrewmate()) || pc.Is(CustomRoles.Madmate))) return false; + if (role is CustomRoles.Loyal && pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeLoyal.GetBool()) return false; + if (role is CustomRoles.Loyal && pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeLoyal.GetBool() || pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Seer && ((pc.GetCustomRole().IsCrewmate() && (!Options.CrewCanBeSeer.GetBool() || pc.Is(CustomRoles.Mortician))) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeSeer.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeSeer.GetBool()) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + if (role is CustomRoles.Necroview && pc.Is(CustomRoles.Doctor) || pc.Is(CustomRoles.God) || pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Bait && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeBait.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeBait.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeBait.GetBool()))) return false; + if (role is CustomRoles.Bewilder && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeBewilder.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeBewilder.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeBewilder.GetBool()))) return false; + if (role is CustomRoles.Sunglasses && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeSunglasses.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeSunglasses.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeSunglasses.GetBool()))) return false; + if (role is CustomRoles.Autopsy && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeAutopsy.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeAutopsy.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeAutopsy.GetBool()))) return false; + if (role is CustomRoles.Glow && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeGlow.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeGlow.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeGlow.GetBool()))) return false; + if (role is CustomRoles.Trapper && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeTrapper.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeTrapper.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeTrapper.GetBool()))) return false; + if (role is CustomRoles.Sidekick && (pc.Is(CustomRoles.Madmate))) return false; + if (role is CustomRoles.Onbound && (pc.Is(CustomRoles.SuperStar))) return false; + if (role is CustomRoles.Rascal && (pc.Is(CustomRoles.SuperStar))) return false; + if (role is CustomRoles.Madmate && (pc.Is(CustomRoles.SuperStar))) return false; + if (role is CustomRoles.Gravestone && (pc.Is(CustomRoles.SuperStar))) return false; +// if (role is CustomRoles.SuperStar && (pc.Is(CustomRoles.Madmate) || pc.Is(CustomRoles.Rascal) || pc.Is(CustomRoles.Onbound))) return false; + if (role is CustomRoles.Autopsy && (pc.Is(CustomRoles.Doctor)) || pc.Is(CustomRoles.Tracefinder) || pc.Is(CustomRoles.Scientist) || pc.Is(CustomRoles.ScientistTOHE) || pc.Is(CustomRoles.Sunnyboy) ) return false; + if (role is CustomRoles.Sidekick && pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeSidekick.GetBool()) return false; + if (role is CustomRoles.Sidekick && pc.GetCustomRole().IsCrewmate() && !Options.CrewmateCanBeSidekick.GetBool()) return false; + if (role is CustomRoles.Sidekick && pc.GetCustomRole().IsImpostor() && !Options.ImpostorCanBeSidekick.GetBool()) return false; + if (role is CustomRoles.Madmate && pc.Is(CustomRoles.Sidekick) || pc.Is(CustomRoles.Egoist)) return false; + if (role is CustomRoles.Sidekick && pc.Is(CustomRoles.Madmate) || pc.Is(CustomRoles.Egoist)) return false; + if (role is CustomRoles.Egoist && pc.Is(CustomRoles.Sidekick) || pc.Is(CustomRoles.Madmate)) return false; + if (role is CustomRoles.Sidekick && pc.Is(CustomRoles.Jackal)) return false; + if (role is CustomRoles.Lucky && pc.Is(CustomRoles.Guardian)) return false; + if (role is CustomRoles.Bait && pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Bait && pc.Is(CustomRoles.Trapper)) return false; + if (role is CustomRoles.Trapper && pc.Is(CustomRoles.Bait)) return false; + if (role is CustomRoles.DualPersonality && pc.Is(CustomRoles.Dictator)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Swooper)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Vampire)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Scavenger)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Puppeteer)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Warlock)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.EvilDiviner)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Witch)) return false; + if (role is CustomRoles.Swift && pc.Is(CustomRoles.Mafia)) return false; + if (role is CustomRoles.Trapper && pc.Is(CustomRoles.GuardianAngelTOHE)) return false; + if (role is CustomRoles.Reach && !pc.CanUseKillButton()) return false; + if (role is CustomRoles.Watcher && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeWatcher.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeWatcher.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeWatcher.GetBool()))) return false; + if (role is CustomRoles.Diseased && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeDiseased.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeDiseased.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeDiseased.GetBool()))) return false; + if (role is CustomRoles.Antidote && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeAntidote.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeAntidote.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeAntidote.GetBool()))) return false; + if (role is CustomRoles.Diseased && pc.Is(CustomRoles.Antidote)) return false; + if (role is CustomRoles.Antidote && pc.Is(CustomRoles.Diseased)) return false; + + if (role is CustomRoles.Necroview && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeNecroview.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeNecroview.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeNecroview.GetBool()))) return false; + if (role is CustomRoles.Oblivious && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeOblivious.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeOblivious.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeOblivious.GetBool()))) return false; + if (role is CustomRoles.Brakar && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeTiebreaker.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeTiebreaker.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeTiebreaker.GetBool()))) return false; + if (role is CustomRoles.Guesser && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeGuesser.GetBool() && pc.Is(CustomRoles.NiceGuesser)) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeGuesser.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeGuesser.GetBool() && pc.Is(CustomRoles.EvilGuesser)))) return false; + if (role is CustomRoles.Onbound && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeOnbound.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeOnbound.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeOnbound.GetBool()))) return false; + // if (role is CustomRoles.Lovers && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeInLove.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeInLove.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeInLove.GetBool()))) return false; + // if (role is CustomRoles.Reflective && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeReflective.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeReflective.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeReflective.GetBool()))) return false; + if (role is CustomRoles.Unreportable && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeUnreportable.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeUnreportable.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeUnreportable.GetBool()))) return false; + if (role is CustomRoles.Lucky && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeLucky.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeLucky.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeLucky.GetBool()))) return false; + if (role is CustomRoles.Unlucky && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeUnlucky.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeUnlucky.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeUnlucky.GetBool()))) return false; + if (role is CustomRoles.Rogue && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeRogue.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeRogue.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeRogue.GetBool()))) return false; + if (role is CustomRoles.Gravestone && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeGravestone.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeGravestone.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeGravestone.GetBool()))) return false; + if (role is CustomRoles.Flashman && pc.Is(CustomRoles.Swooper)) return false; + // if (role is CustomRoles.Lovers && pc.Is(CustomRoles.Dictator)) return false; + // if (role is CustomRoles.Lovers && pc.Is(CustomRoles.Dictator)) return false; + if (role is CustomRoles.Bait && pc.Is(CustomRoles.Unreportable)) return false; + // if (role is CustomRoles.Onbound && pc.Is(CustomRoles.Reflective)) return false; + // if (role is CustomRoles.Reflective && pc.Is(CustomRoles.Onbound)) return false; + if (role is CustomRoles.Unreportable && pc.Is(CustomRoles.Bait)) return false; + // if (role is CustomRoles.Lovers && pc.Is(CustomRoles.Provocateur)) return false; + if (role is CustomRoles.Oblivious && pc.Is(CustomRoles.Bloodhound)) return false; + if (role is CustomRoles.Oblivious && pc.Is(CustomRoles.Vulture)) return false; + if (role is CustomRoles.Vulture && pc.Is(CustomRoles.Oblivious)) return false; + if (role is CustomRoles.Guesser && (pc.Is(CustomRoles.CopyCat) || pc.Is(CustomRoles.Doomsayer))) return false; + if (role is CustomRoles.CopyCat && pc.Is(CustomRoles.Guesser)) return false; + if (role is CustomRoles.DoubleShot && pc.Is(CustomRoles.CopyCat)) return false; + if (role is CustomRoles.CopyCat && pc.Is(CustomRoles.DoubleShot)) return false; + + if (role is CustomRoles.Brakar && pc.Is(CustomRoles.Dictator)) return false; + if (role is CustomRoles.Lucky && pc.Is(CustomRoles.Luckey)) return false; + if (role is CustomRoles.Unlucky && pc.Is(CustomRoles.Luckey)) return false; + if (role is CustomRoles.Unlucky && pc.Is(CustomRoles.Lucky)) return false; + if (role is CustomRoles.Lucky && pc.Is(CustomRoles.Unlucky)) return false; + if (role is CustomRoles.Fool && ((pc.GetCustomRole().IsCrewmate() && !Options.CrewCanBeFool.GetBool()) || (pc.GetCustomRole().IsNeutral() && !Options.NeutralCanBeFool.GetBool()) || (pc.GetCustomRole().IsImpostor() && !Options.ImpCanBeFool.GetBool()) || pc.Is(CustomRoles.SabotageMaster) || pc.Is(CustomRoles.GuardianAngelTOHE))) return false; + if (role is CustomRoles.Bloodhound && pc.Is(CustomRoles.Oblivious)) return false; + if (role is CustomRoles.DoubleShot && ((pc.Is(CustomRoleTypes.Impostor) && !Options.ImpCanBeDoubleShot.GetBool()) || (pc.Is(CustomRoleTypes.Crewmate) && !Options.CrewCanBeDoubleShot.GetBool()) || (pc.Is(CustomRoleTypes.Neutral) && !Options.NeutralCanBeDoubleShot.GetBool()))) return false; + if (role is CustomRoles.DoubleShot && (!pc.Is(CustomRoles.EvilGuesser) && !pc.Is(CustomRoles.NiceGuesser) && !pc.Is(CustomRoles.Guesser) && !Options.GuesserMode.GetBool())) return false; + if (role is CustomRoles.DoubleShot && Options.ImpCanBeDoubleShot.GetBool() && !pc.Is(CustomRoles.Guesser) && !pc.Is(CustomRoles.EvilGuesser) && (pc.Is(CustomRoleTypes.Impostor) && !Options.ImpostorsCanGuess.GetBool())) return false; + if (role is CustomRoles.DoubleShot && Options.CrewCanBeDoubleShot.GetBool() && !pc.Is(CustomRoles.Guesser) && !pc.Is(CustomRoles.NiceGuesser) && (pc.Is(CustomRoleTypes.Crewmate) && !Options.CrewmatesCanGuess.GetBool())) return false; + if (role is CustomRoles.DoubleShot && Options.NeutralCanBeDoubleShot.GetBool() && !pc.Is(CustomRoles.Guesser) && ((pc.GetCustomRole().IsNonNK() && !Options.PassiveNeutralsCanGuess.GetBool()) || (pc.GetCustomRole().IsNK() && !Options.NeutralKillersCanGuess.GetBool()))) return false; + return true; + } + public static RoleTypes GetRoleTypes(this CustomRoles role) + => GetVNRole(role) switch + { + CustomRoles.Impostor => RoleTypes.Impostor, + CustomRoles.Scientist => RoleTypes.Scientist, + CustomRoles.Engineer => RoleTypes.Engineer, + CustomRoles.GuardianAngel => RoleTypes.GuardianAngel, + CustomRoles.Shapeshifter => RoleTypes.Shapeshifter, + CustomRoles.Crewmate => RoleTypes.Crewmate, + _ => role.IsImpostor() ? RoleTypes.Impostor : RoleTypes.Crewmate, + }; + public static bool IsDesyncRole(this CustomRoles role) => role.GetDYRole() != RoleTypes.GuardianAngel; + public static bool IsImpostorTeam(this CustomRoles role) => role.IsImpostor() || role == CustomRoles.Madmate; + public static bool IsCrewmate(this CustomRoles role) => !role.IsImpostor() && !role.IsNeutral() && !role.IsMadmate(); + + public static bool IsImpostorTeamV2(this CustomRoles role) => (role.IsImpostorTeam() && role != CustomRoles.Trickster && !role.IsConverted()) || role == CustomRoles.Rascal; + public static bool IsNeutralTeamV2(this CustomRoles role) => (role.IsConverted() || role.IsNeutral() && role != CustomRoles.Madmate); + + public static bool IsCrewmateTeamV2(this CustomRoles role) => ((!role.IsImpostorTeamV2() && !role.IsNeutralTeamV2()) || (role == CustomRoles.Trickster && !role.IsConverted())); + + public static bool IsConverted(this CustomRoles role) + { + + return (role is CustomRoles.Charmed || + role is CustomRoles.Recruit || + role is CustomRoles.Infected || + role is CustomRoles.Contagious || + role is CustomRoles.Lovers || + ((role is CustomRoles.Egoist) && (ParityCop.ParityCheckEgoistInt() == 1))); + } + public static bool IsRevealingRole(this CustomRoles role, PlayerControl target) + { + return (((role is CustomRoles.Mayor) && (Options.MayorRevealWhenDoneTasks.GetBool()) && target.AllTasksCompleted()) || + ((role is CustomRoles.SuperStar) && (Options.EveryOneKnowSuperStar.GetBool())) || + ((role is CustomRoles.Marshall) && target.AllTasksCompleted()) || + ((role is CustomRoles.Workaholic) && (Options.WorkaholicVisibleToEveryone.GetBool())) || + ((role is CustomRoles.Doctor) && (Options.DoctorVisibleToEveryone.GetBool())) || + ((role is CustomRoles.Bait) && (Options.BaitNotification.GetBool()) && ParityCop.ParityCheckBaitCountType.GetBool())); + } + public static bool IsImpostorTeamV3(this CustomRoles role) => (role.IsImpostor() || role.IsMadmate()); + public static bool IsNeutralKillerTeam(this CustomRoles role) => (role.IsNK() || !role.IsMadmate()); + public static bool IsPassiveNeutralTeam(this CustomRoles role) => (role.IsNonNK() || !role.IsMadmate()); + public static bool IsNNK(this CustomRoles role) => role.IsNeutral() && !role.IsNK(); + public static bool IsVanilla(this CustomRoles role) + { + return role is + CustomRoles.Crewmate or + CustomRoles.Engineer or + CustomRoles.Scientist or + CustomRoles.GuardianAngel or + CustomRoles.Impostor or + CustomRoles.Shapeshifter; + } + public static CustomRoleTypes GetCustomRoleTypes(this CustomRoles role) + { + CustomRoleTypes type = CustomRoleTypes.Crewmate; + if (role.IsImpostor()) type = CustomRoleTypes.Impostor; + if (role.IsNeutral()) type = CustomRoleTypes.Neutral; + // if (role.IsMadmate()) type = CustomRoleTypes.Madmate; + if (role.IsAdditionRole()) type = CustomRoleTypes.Addon; + return type; + } + public static bool RoleExist(this CustomRoles role, bool countDead = false) => Main.AllPlayerControls.Any(x => x.Is(role) && (x.IsAlive() || countDead)); + public static int GetCount(this CustomRoles role) + { + if (role.IsVanilla()) + { + if (Options.DisableVanillaRoles.GetBool()) return 0; + var roleOpt = Main.NormalOptions.RoleOptions; + return role switch + { + CustomRoles.Engineer => roleOpt.GetNumPerGame(RoleTypes.Engineer), + CustomRoles.Scientist => roleOpt.GetNumPerGame(RoleTypes.Scientist), + CustomRoles.Shapeshifter => roleOpt.GetNumPerGame(RoleTypes.Shapeshifter), + CustomRoles.GuardianAngel => roleOpt.GetNumPerGame(RoleTypes.GuardianAngel), + CustomRoles.Crewmate => roleOpt.GetNumPerGame(RoleTypes.Crewmate), + _ => 0 + }; + } + else + { + return Options.GetRoleCount(role); + } + } + public static int GetMode(this CustomRoles role) => Options.GetRoleSpawnMode(role); + public static float GetChance(this CustomRoles role) + { + if (role.IsVanilla()) + { + var roleOpt = Main.NormalOptions.RoleOptions; + return role switch + { + CustomRoles.Engineer => roleOpt.GetChancePerGame(RoleTypes.Engineer), + CustomRoles.Scientist => roleOpt.GetChancePerGame(RoleTypes.Scientist), + CustomRoles.Shapeshifter => roleOpt.GetChancePerGame(RoleTypes.Shapeshifter), + CustomRoles.GuardianAngel => roleOpt.GetChancePerGame(RoleTypes.GuardianAngel), + CustomRoles.Crewmate => roleOpt.GetChancePerGame(RoleTypes.Crewmate), + _ => 0 + } / 100f; + } + else + { + return Options.GetRoleChance(role); + } + } + public static bool IsEnable(this CustomRoles role) => role.GetCount() > 0; + public static CountTypes GetCountTypes(this CustomRoles role) + => role switch + { + CustomRoles.GM => CountTypes.OutOfGame, + CustomRoles.Jackal => CountTypes.Jackal, + CustomRoles.Sidekick => CountTypes.Jackal, + CustomRoles.Poisoner => CountTypes.Poisoner, + CustomRoles.Pelican => CountTypes.Pelican, + CustomRoles.Gamer => CountTypes.Gamer, + CustomRoles.BloodKnight => CountTypes.BloodKnight, + CustomRoles.Succubus => CountTypes.Succubus, + CustomRoles.HexMaster => CountTypes.HexMaster, + CustomRoles.NWitch => CountTypes.NWitch, + CustomRoles.Wraith => CountTypes.Wraith, + CustomRoles.Pestilence => CountTypes.Pestilence, + CustomRoles.PlagueBearer => CountTypes.PlagueBearer, + CustomRoles.Parasite => CountTypes.Impostor, + CustomRoles.NSerialKiller => CountTypes.NSerialKiller, + CustomRoles.Juggernaut => CountTypes.Juggernaut, + CustomRoles.Jinx => CountTypes.Jinx, + CustomRoles.Infectious => CountTypes.Infectious, + CustomRoles.Crewpostor => CountTypes.Impostor, + CustomRoles.Virus => CountTypes.Virus, + CustomRoles.Ritualist => CountTypes.Ritualist, + CustomRoles.Pickpocket => CountTypes.Pickpocket, + CustomRoles.Traitor => CountTypes.Traitor, + CustomRoles.Medusa => CountTypes.Medusa, + CustomRoles.Refugee => CountTypes.Impostor, + CustomRoles.Glitch => CountTypes.Glitch, + // CustomRoles.Phantom => CountTypes.OutOfGame, + // CustomRoles.CursedSoul => CountTypes.OutOfGame, // if they count as OutOfGame, it prevents them from winning lmao + + CustomRoles.Spiritcaller => CountTypes.Spiritcaller, + _ => role.IsImpostorTeam() ? CountTypes.Impostor : CountTypes.Crew, + }; + + public static bool HasSubRole(this PlayerControl pc) => Main.PlayerStates[pc.PlayerId].SubRoles.Count > 0; +} +public enum CustomRoleTypes +{ + Crewmate, + Impostor, + Neutral, + Addon +} +public enum CountTypes +{ + OutOfGame, + None, + Crew, + Impostor, + Jackal, + Pelican, + Gamer, + BloodKnight, + Poisoner, + Charmed, + Succubus, + HexMaster, + NWitch, + Wraith, + NSerialKiller, + Juggernaut, + Infectious, + Virus, + Rogue, + DarkHide, + Jinx, + Ritualist, + Pickpocket, + Traitor, + Medusa, + Spiritcaller, + Pestilence, + PlagueBearer, + Glitch, + Arsonist +} \ No newline at end of file diff --git a/Modules/CustomRpcSender.cs b/Modules/CustomRpcSender.cs new file mode 100644 index 000000000..126b32938 --- /dev/null +++ b/Modules/CustomRpcSender.cs @@ -0,0 +1,267 @@ +using AmongUs.GameOptions; +using Hazel; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using System; +namespace TOHE; + +public class CustomRpcSender +{ + public MessageWriter stream; + public readonly string name; + public readonly SendOption sendOption; + public bool isUnsafe; + public delegate void onSendDelegateType(); + public onSendDelegateType onSendDelegate; + + public State CurrentState + { + get { return currentState; } + set + { + if (isUnsafe) currentState = value; + else Logger.Warn("CurrentStateはisUnsafeがtrueの時のみ上書きできます", "CustomRpcSender"); + } + } + private State currentState = State.BeforeInit; + + //0~: targetClientId (GameDataTo) + //-1: 全プレイヤー (GameData) + //-2: 未設定 + private int currentRpcTarget; + + private CustomRpcSender() { } + public CustomRpcSender(string name, SendOption sendOption, bool isUnsafe) + { + stream = MessageWriter.Get(sendOption); + + this.name = name; + this.sendOption = sendOption; + this.isUnsafe = isUnsafe; + this.currentRpcTarget = -2; + onSendDelegate = () => Logger.Info($"{this.name}'s onSendDelegate =>", "CustomRpcSender"); + + currentState = State.Ready; + Logger.Info($"\"{name}\" is ready", "CustomRpcSender"); + } + public static CustomRpcSender Create(string name = "No Name Sender", SendOption sendOption = SendOption.None, bool isUnsafe = false) + { + return new CustomRpcSender(name, sendOption, isUnsafe); + } + + #region Start/End Message + public CustomRpcSender StartMessage(int targetClientId = -1) + { + if (currentState != State.Ready) + { + string errorMsg = $"Messageを開始しようとしましたが、StateがReadyではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + + if (targetClientId < 0) + { + // 全員に対するRPC + stream.StartMessage(5); + stream.Write(AmongUsClient.Instance.GameId); + } + else + { + // 特定のクライアントに対するRPC (Desync) + stream.StartMessage(6); + stream.Write(AmongUsClient.Instance.GameId); + stream.WritePacked(targetClientId); + } + + currentRpcTarget = targetClientId; + currentState = State.InRootMessage; + return this; + } + public CustomRpcSender EndMessage(int targetClientId = -1) + { + if (currentState != State.InRootMessage) + { + string errorMsg = $"Messageを終了しようとしましたが、StateがInRootMessageではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + stream.EndMessage(); + + currentRpcTarget = -2; + currentState = State.Ready; + return this; + } + #endregion + #region Start/End Rpc + public CustomRpcSender StartRpc(uint targetNetId, RpcCalls rpcCall) + => StartRpc(targetNetId, (byte)rpcCall); + public CustomRpcSender StartRpc( + uint targetNetId, + byte callId) + { + if (currentState != State.InRootMessage) + { + string errorMsg = $"RPCを開始しようとしましたが、StateがInRootMessageではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + + stream.StartMessage(2); + stream.WritePacked(targetNetId); + stream.Write(callId); + + currentState = State.InRpc; + return this; + } + public CustomRpcSender EndRpc() + { + if (currentState != State.InRpc) + { + string errorMsg = $"RPCを終了しようとしましたが、StateがInRpcではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + + stream.EndMessage(); + currentState = State.InRootMessage; + return this; + } + #endregion + public CustomRpcSender AutoStartRpc( + uint targetNetId, + byte callId, + int targetClientId = -1) + { + if (targetClientId == -2) targetClientId = -1; + if (currentState is not State.Ready and not State.InRootMessage) + { + string errorMsg = $"RPCを自動で開始しようとしましたが、StateがReadyまたはInRootMessageではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + if (currentRpcTarget != targetClientId) + { + //StartMessage処理 + if (currentState == State.InRootMessage) this.EndMessage(); + this.StartMessage(targetClientId); + } + this.StartRpc(targetNetId, callId); + + return this; + } + public void SendMessage() + { + if (currentState == State.InRootMessage) this.EndMessage(); + if (currentState != State.Ready) + { + string errorMsg = $"RPCを送信しようとしましたが、StateがReadyではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + + AmongUsClient.Instance.SendOrDisconnect(stream); + onSendDelegate(); + currentState = State.Finished; + Logger.Info($"\"{name}\" is finished", "CustomRpcSender"); + stream.Recycle(); + } + + // Write + #region PublicWriteMethods + public CustomRpcSender Write(float val) => Write(w => w.Write(val)); + public CustomRpcSender Write(string val) => Write(w => w.Write(val)); + public CustomRpcSender Write(ulong val) => Write(w => w.Write(val)); + public CustomRpcSender Write(int val) => Write(w => w.Write(val)); + public CustomRpcSender Write(uint val) => Write(w => w.Write(val)); + public CustomRpcSender Write(ushort val) => Write(w => w.Write(val)); + public CustomRpcSender Write(byte val) => Write(w => w.Write(val)); + public CustomRpcSender Write(sbyte val) => Write(w => w.Write(val)); + public CustomRpcSender Write(bool val) => Write(w => w.Write(val)); + public CustomRpcSender Write(Il2CppStructArray bytes) => Write(w => w.Write(bytes)); + public CustomRpcSender Write(Il2CppStructArray bytes, int offset, int length) => Write(w => w.Write(bytes, offset, length)); + public CustomRpcSender WriteBytesAndSize(Il2CppStructArray bytes) => Write(w => w.WriteBytesAndSize(bytes)); + public CustomRpcSender WritePacked(int val) => Write(w => w.WritePacked(val)); + public CustomRpcSender WritePacked(uint val) => Write(w => w.WritePacked(val)); + public CustomRpcSender WriteNetObject(InnerNetObject obj) => Write(w => w.WriteNetObject(obj)); + #endregion + + private CustomRpcSender Write(Action action) + { + if (currentState != State.InRpc) + { + string errorMsg = $"RPCを書き込もうとしましたが、StateがWrite(書き込み中)ではありません (in: \"{name}\")"; + if (isUnsafe) + { + Logger.Warn(errorMsg, "CustomRpcSender.Warn"); + } + else + { + throw new InvalidOperationException(errorMsg); + } + } + action(stream); + + return this; + } + + public enum State + { + BeforeInit = 0, //初期化前 何もできない + Ready, //送信準備完了 StartMessageとSendMessageを実行可能 + InRootMessage, //StartMessage~EndMessageの間の状態 StartRpcとEndMessageを実行可能 + InRpc, //StartRpc~EndRpcの間の状態 WriteとEndRpcを実行可能 + Finished, //送信後 何もできない + } +} + +public static class CustomRpcSenderExtensions +{ + public static void RpcSetRole(this CustomRpcSender sender, PlayerControl player, RoleTypes role, int targetClientId = -1) + { + sender.AutoStartRpc(player.NetId, (byte)RpcCalls.SetRole, targetClientId) + .Write((ushort)role) + .EndRpc(); + } + public static void RpcMurderPlayerV3(this CustomRpcSender sender, PlayerControl player, PlayerControl target, int targetClientId = -1) + { + sender.AutoStartRpc(player.NetId, (byte)RpcCalls.MurderPlayer, targetClientId) + .WriteNetObject(target) + .EndRpc(); + } +} \ No newline at end of file diff --git a/Modules/CustomSounds.cs b/Modules/CustomSounds.cs new file mode 100644 index 000000000..f510ea51c --- /dev/null +++ b/Modules/CustomSounds.cs @@ -0,0 +1,64 @@ +using Hazel; +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using static UnityEngine.ParticleSystem.PlaybackState; + +namespace TOHE.Modules; + +public static class CustomSoundsManager +{ + public static void RPCPlayCustomSound(this PlayerControl pc, string sound, bool force = false) + { + if (!force) if (!AmongUsClient.Instance.AmHost || !pc.IsModClient()) return; + if (pc == null || PlayerControl.LocalPlayer.PlayerId == pc.PlayerId) + { + Play(sound); + return; + } + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlayCustomSound, Hazel.SendOption.Reliable, pc.GetClientId()); + writer.Write(sound); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void RPCPlayCustomSoundAll(string sound) + { + if (!AmongUsClient.Instance.AmHost) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlayCustomSound, Hazel.SendOption.Reliable, -1); + writer.Write(sound); + AmongUsClient.Instance.FinishRpcImmediately(writer); + Play(sound); + } + public static void ReceiveRPC(MessageReader reader) => Play(reader.ReadString()); + + + private static readonly string SOUNDS_PATH = @$"{Environment.CurrentDirectory.Replace(@"\", "/")}/BepInEx/resources/"; + public static void Play(string sound) + { + if (!Constants.ShouldPlaySfx() || !Main.EnableCustomSoundEffect.Value) return; + var path = SOUNDS_PATH + sound + ".wav"; + if (!Directory.Exists(SOUNDS_PATH)) Directory.CreateDirectory(SOUNDS_PATH); + DirectoryInfo folder = new(SOUNDS_PATH); + if ((folder.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden) + folder.Attributes = FileAttributes.Hidden; + if (!File.Exists(path)) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("TOHE.Resources.Sounds." + sound + ".wav"); + if (stream == null) + { + Logger.Warn($"声音文件缺失:{sound}", "CustomSounds"); + return; + } + var fs = File.Create(path); + stream.CopyTo(fs); + fs.Close(); + } + StartPlay(path); + Logger.Msg($"播放声音:{sound}", "CustomSounds"); + } + + [DllImport("winmm.dll")] + public static extern bool PlaySound(string Filename, int Mod, int Flags); + public static void StartPlay(string path) => PlaySound(@$"{path}", 0, 1); //第3个形参,把1换为9,连续播放 + +} diff --git a/Modules/CustomWinnerHolder.cs b/Modules/CustomWinnerHolder.cs new file mode 100644 index 000000000..de4da3ed1 --- /dev/null +++ b/Modules/CustomWinnerHolder.cs @@ -0,0 +1,91 @@ +using Hazel; +using System.Collections.Generic; + +namespace TOHE; + +public static class CustomWinnerHolder +{ + // 勝者のチームが格納されます。 + // リザルトの背景色の決定などに使用されます。 + // 注: この変数を変更する時、WinnerRoles・WinnerIdsを同時に変更しないと予期せぬ勝者が現れる可能性があります。 + public static CustomWinner WinnerTeam; + // 追加勝利するプレイヤーのチームが格納されます。 + // リザルトの表示に使用されます。 + public static HashSet AdditionalWinnerTeams; + // 勝者の役職が格納され、この変数に格納されている役職のプレイヤーは全員勝利となります。 + // チームとなるニュートラルの処理に最適です。 + public static HashSet WinnerRoles; + // 勝者のPlayerIDが格納され、このIDを持つプレイヤーは全員勝利します。 + // 単独勝利するニュートラルの処理に最適です。 + public static HashSet WinnerIds; + + public static void Reset() + { + WinnerTeam = CustomWinner.Default; + AdditionalWinnerTeams = new(); + WinnerRoles = new(); + WinnerIds = new(); + } + public static void ClearWinners() + { + WinnerRoles.Clear(); + WinnerIds.Clear(); + } + /// WinnerTeamに値を代入します。すでに代入されている場合、AdditionalWinnerTeamsに追加します。 + public static void SetWinnerOrAdditonalWinner(CustomWinner winner) + { + if (WinnerTeam == CustomWinner.Default) WinnerTeam = winner; + else AdditionalWinnerTeams.Add((AdditionalWinners)winner); + } + /// WinnerTeamに値を代入します。すでに代入されている場合、既存の値をAdditionalWinnerTeamsに追加してから代入します。 + public static void ShiftWinnerAndSetWinner(CustomWinner winner) + { + if (WinnerTeam != CustomWinner.Default) + AdditionalWinnerTeams.Add((AdditionalWinners)WinnerTeam); + WinnerTeam = winner; + } + /// 既存の値をすべて削除してから、WinnerTeamに値を代入します。 + public static void ResetAndSetWinner(CustomWinner winner) + { + Reset(); + WinnerTeam = winner; + } + + public static MessageWriter WriteTo(MessageWriter writer) + { + writer.Write((int)WinnerTeam); + + writer.Write(AdditionalWinnerTeams.Count); + foreach (var wt in AdditionalWinnerTeams) + writer.Write((int)wt); + + writer.Write(WinnerRoles.Count); + foreach (var wr in WinnerRoles) + writer.Write((int)wr); + + writer.Write(WinnerIds.Count); + foreach (var id in WinnerIds) + writer.Write(id); + + return writer; + } + public static void ReadFrom(MessageReader reader) + { + WinnerTeam = (CustomWinner)reader.ReadInt32(); + + AdditionalWinnerTeams = new(); + int AdditionalWinnerTeamsCount = reader.ReadInt32(); + for (int i = 0; i < AdditionalWinnerTeamsCount; i++) + AdditionalWinnerTeams.Add((AdditionalWinners)reader.ReadInt32()); + + WinnerRoles = new(); + int WinnerRolesCount = reader.ReadInt32(); + for (int i = 0; i < WinnerRolesCount; i++) + WinnerRoles.Add((CustomRoles)reader.ReadInt32()); + + WinnerIds = new(); + int WinnerIdsCount = reader.ReadInt32(); + for (int i = 0; i < WinnerIdsCount; i++) + WinnerIds.Add(reader.ReadByte()); + } +} \ No newline at end of file diff --git a/Modules/DebugModeManager.cs b/Modules/DebugModeManager.cs new file mode 100644 index 000000000..1bb0904be --- /dev/null +++ b/Modules/DebugModeManager.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace TOHE; + +public static class DebugModeManager +{ + // これが有効の時、通常のゲームに支障のないデバッグ機能(詳細ログ・ゲーム外でのデバッグ表示など)が有効化される。 + // また、ゲーム内オプションでデバッグモードを有効化することができる。 + public static bool AmDebugger { get; private set; } = +#if DEBUG +true; +#else +false; +#endif + // これが有効の時、通常のゲームを破壊する可能性のある強力なデバッグ機能(テレポートなど)が有効化される。 + public static bool IsDebugMode => AmDebugger && EnableDebugMode != null && EnableDebugMode.GetBool(); + + public static OptionItem EnableDebugMode; + + public static void Auth(HashAuth auth, string input) + { + // AmDebugger = デバッグビルドである || デバッグキー認証が通った + AmDebugger = AmDebugger || auth.CheckString(input); + } + public static void SetupCustomOption() + { + EnableDebugMode = BooleanOptionItem.Create(2, "EnableDebugMode", false, TabGroup.SystemSettings, true) + .SetHeader(true) + .SetColor(Color.green) + .SetHeader(true) + .SetHidden(!AmDebugger); + } +} \ No newline at end of file diff --git a/Modules/Debugger.cs b/Modules/Debugger.cs new file mode 100644 index 000000000..167427333 --- /dev/null +++ b/Modules/Debugger.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Runtime.CompilerServices; +using TOHE.Modules; +using LogLevel = BepInEx.Logging.LogLevel; + +namespace TOHE; + +class Webhook +{ + public static void Send(string text) + { + if (Main.WebhookURL.Value == "none") return; + HttpClient httpClient = new(); + Dictionary strs = new() + { + { "content", text }, + { "username", "TOHE-Debugger" }, + { "avatar_url", "https://npm.elemecdn.com/hexo-static@1.0.1/img/avatar.webp" } + }; + TaskAwaiter awaiter = httpClient.PostAsync( + Main.WebhookURL.Value, new FormUrlEncodedContent(strs)).GetAwaiter(); + awaiter.GetResult(); + } +} +class Logger +{ + public static bool isEnable; + public static List disableList = new(); + public static List sendToGameList = new(); + public static bool isDetail = false; + public static bool isAlsoInGame = false; + public static void Enable() => isEnable = true; + public static void Disable() => isEnable = false; + public static void Enable(string tag, bool toGame = false) + { + disableList.Remove(tag); + if (toGame && !sendToGameList.Contains(tag)) sendToGameList.Add(tag); + else sendToGameList.Remove(tag); + } + public static void Disable(string tag) { if (!disableList.Contains(tag)) disableList.Add(tag); } + public static void SendInGame(string text, bool isAlways = false) + { + if (!isEnable) return; + if (DestroyableSingleton._instance) DestroyableSingleton.Instance.Notifier.AddItem(text); + } + private static void SendToFile(string text, LogLevel level = LogLevel.Info, string tag = "", bool escapeCRLF = true, int lineNumber = 0, string fileName = "") + { + if (!isEnable || disableList.Contains(tag)) return; + var logger = Main.Logger; + string t = DateTime.Now.ToString("HH:mm:ss"); + if (sendToGameList.Contains(tag) || isAlsoInGame) SendInGame($"[{tag}]{text}"); + if (escapeCRLF) + text = text.Replace("\r", "\\r").Replace("\n", "\\n"); + string log_text = $"[{t}][{tag}]{text}"; + if (isDetail && DebugModeManager.AmDebugger) + { + StackFrame stack = new(2); + string className = stack.GetMethod().ReflectedType.Name; + string memberName = stack.GetMethod().Name; + log_text = $"[{t}][{className}.{memberName}({Path.GetFileName(fileName)}:{lineNumber})][{tag}]{text}"; + } + switch (level) + { + case LogLevel.Info: + logger.LogInfo(log_text); + break; + case LogLevel.Warning: + logger.LogWarning(log_text); + break; + case LogLevel.Error: + logger.LogError(log_text); + break; + case LogLevel.Fatal: + logger.LogFatal(log_text); + break; + case LogLevel.Message: + logger.LogMessage(log_text); + break; + case LogLevel.Debug: + logger.LogFatal(log_text); + break; + default: + logger.LogWarning("Error:Invalid LogLevel"); + logger.LogInfo(log_text); + break; + } + } + public static void Test(object content, string tag = "======= Test =======", bool escapeCRLF = true, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(content.ToString(), LogLevel.Debug, tag, escapeCRLF, lineNumber, fileName); + public static void Info(string text, string tag, bool escapeCRLF = true, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(text, LogLevel.Info, tag, escapeCRLF, lineNumber, fileName); + public static void Warn(string text, string tag, bool escapeCRLF = true, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(text, LogLevel.Warning, tag, escapeCRLF, lineNumber, fileName); + public static void Error(string text, string tag, bool escapeCRLF = true, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(text, LogLevel.Error, tag, escapeCRLF, lineNumber, fileName); + public static void Fatal(string text, string tag, bool escapeCRLF = true, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(text, LogLevel.Fatal, tag, escapeCRLF, lineNumber, fileName); + public static void Msg(string text, string tag, bool escapeCRLF = true, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(text, LogLevel.Message, tag, escapeCRLF, lineNumber, fileName); + public static void Exception(Exception ex, string tag, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") => + SendToFile(ex.ToString(), LogLevel.Error, tag, false, lineNumber, fileName); + public static void CurrentMethod([CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "") + { + StackFrame stack = new(1); + Logger.Msg($"\"{stack.GetMethod().ReflectedType.Name}.{stack.GetMethod().Name}\" Called in \"{Path.GetFileName(fileName)}({lineNumber})\"", "Method"); + } + + public static LogHandler Handler(string tag) + => new(tag); +} \ No newline at end of file diff --git a/Modules/DevManager.cs b/Modules/DevManager.cs new file mode 100644 index 000000000..a2884f8de --- /dev/null +++ b/Modules/DevManager.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TOHE; + +public class DevUser +{ + public string Code { get; set; } + public string Color { get; set; } + public string Tag { get; set; } + public bool IsUp { get; set; } + public bool IsDev { get; set; } + public bool DeBug { get; set; } + public bool ColorCmd { get; set; } + public string UpName { get; set; } + public DevUser(string code = "", string color = "null", string tag = "null", bool isUp = false, bool isDev = false, bool deBug = false, bool colorCmd = false, string upName = "未认证用户") + { + Code = code; + Color = color; + Tag = tag; + IsUp = isUp; + IsDev = isDev; + DeBug = deBug; + ColorCmd = colorCmd; + UpName = upName; + } + public bool HasTag() => Tag != "null"; + public string GetTag() => Color == "null" ? $"{Tag}\r\n" : $"{(Tag == "#Dev" ? Translator.GetString("Developer") : Tag)}\r\n"; +} + +public static class DevManager +{ + public static DevUser DefaultDevUser = new(); + public static List DevUserList = new(); + public static void Init() + { + // Dev + DevUserList.Add(new(code: "actorour#0029", color: "#ffc0cb", tag: "Original Developer", isUp: true, isDev: true, deBug: true, colorCmd: true, upName: "KARPED1EM")); + DevUserList.Add(new(code: "pinklaze#1776", color: "#30548e", tag: "#Dev", isUp: true, isDev: true, deBug: true, colorCmd: false, upName: "NCSIMON")); + DevUserList.Add(new(code: "keepchirpy#6354", color: "#1FF3C6", tag: "Переводчик", isUp: true, isDev: true, deBug: true, colorCmd: true, upName: "TommyXL")); //Tommy-XL + DevUserList.Add(new(code: "taskunsold#2701", color: "null", tag: "Temmie", isUp: false, isDev: true, deBug: false, colorCmd: false, upName: null)); //Tem + DevUserList.Add(new(code: "timedapper#9496", color: "#48FFFF", tag: "#Dev", isUp: false, isDev: true, deBug: false, colorCmd: false, upName: null)); //阿龍 + DevUserList.Add(new(code: "sofaagile#3120", color: "null", tag: "null", isUp: false, isDev: true, deBug: true, colorCmd: false, upName: null)); //天寸 + DevUserList.Add(new(code: "keyscreech#2151", color: "null", tag: "美術NotKomi", isUp: false, isDev: true, deBug: false, upName: null)); //Endrmen40409 + + // Up + DevUserList.Add(new(code: "truantwarm#9165", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "萧暮不姓萧")); + DevUserList.Add(new(code: "drilldinky#1386", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "爱玩AU的河豚")); + DevUserList.Add(new(code: "farardour#6818", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "-提米SaMa-")); + DevUserList.Add(new(code: "vealused#8192", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "lag丶xy")); + DevUserList.Add(new(code: "storyeager#0815", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "航娜丽莎")); + DevUserList.Add(new(code: "versegame#3885", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "柴唔cw")); + DevUserList.Add(new(code: "closegrub#6217", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "警长不会玩")); + DevUserList.Add(new(code: "frownnatty#7935", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "鬼灵official")); + DevUserList.Add(new(code: "veryscarf#5368", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "小武同学102")); + DevUserList.Add(new(code: "sparklybee#0275", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "--红包SaMa--")); + DevUserList.Add(new(code: "endingyon#3175", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "游侠开摆")); + DevUserList.Add(new(code: "firmine#0232", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "YH永恒_")); + DevUserList.Add(new(code: "storkfey#3570", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "Calypso")); + DevUserList.Add(new(code: "fellowsand#1003", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "C-Faust")); + DevUserList.Add(new(code: "jetsafe#8512", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "Hoream是好人")); + DevUserList.Add(new(code: "primether#5348", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "AnonWorks")); + DevUserList.Add(new(code: "spoonkey#0792", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "没好康的")); + DevUserList.Add(new(code: "beakedmire#6099", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "茄-au")); + DevUserList.Add(new(code: "doggedsize#7892", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "TronAndRey")); + DevUserList.Add(new(code: "openlanded#9533", color: "#9e2424", tag: "God Of Death Love Apples", isUp: true, isDev: true, deBug: true, colorCmd: true, upName: "ryuk")); + DevUserList.Add(new(code: "unlikecity#4086", color: "#eD2F91", tag: "Ward", isUp: true, isDev: false, deBug: false, colorCmd: true, upName: "Ward")); + DevUserList.Add(new(code: "iconicdrop#2727", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: true, upName: "jackler")); + + DevUserList.Add(new(code: "neatnet#5851", color: "#FFFF00", tag: "The 200IQ guy", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "The 200IQ guy")); + DevUserList.Add(new(code: "contenthue#0404", color: "#FFFF00", tag: "The 200IQ guy", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "The 200IQ guy")); + DevUserList.Add(new(code: "heavyclod#2286", color: "#FFFF00", tag: "小叨.exe已停止运行", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "小叨院长")); + DevUserList.Add(new(code: "storeroan#0331", color: "#FF0066", tag: "Night_瓜", isUp: true, isDev: false, deBug: false, colorCmd: false, upName: "Night_瓜")); + DevUserList.Add(new(code: "teamelder#5856", color: "#1379bf", tag: "屑Slok(没信誉的鸽子)", isUp: true, isDev: false, colorCmd: false, deBug: false, upName: "Slok7565")); + + DevUserList.Add(new(code: "radarright#2509", color: "null", tag: "null", isUp: false, isDev: false, deBug: true, colorCmd: false, upName: null)); + + // Sponsor + DevUserList.Add(new(code: "recentduct#6068", color: "#FF00FF", tag: "高冷男模法师", isUp: false, isDev: false, colorCmd: false, deBug: true, upName: null)); + DevUserList.Add(new(code: "canneddrum#2370", color: "#fffcbe", tag: "我是喜唉awa", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "dovefitted#5329", color: "#1379bf", tag: "不要首刀我", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "luckylogo#7352", color: "#f30000", tag: "林@林", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "axefitful#8788", color: "#8e8171", tag: "寄才是真理", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "raftzonal#8893", color: "#8e8171", tag: "寄才是真理", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "twainrobin#8089", color: "#0000FF", tag: "啊哈修maker", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "mallcasual#6075", color: "#f89ccb", tag: "波奇酱", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "beamelfin#9478", color: "#6495ED", tag: "Amaster-1111", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); + DevUserList.Add(new(code: "lordcosy#8966", color: "#FFD6EC", tag: "HostTOHE", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); //K +// DevUserList.Add(new(code: "honestsofa#2870", color: "#D381D9", tag: "Discord: SolarFlare#0700", isUp: true, isDev: false, colorCmd: false, deBug: false, upName: "SolarFlare")); //SolarFlare + DevUserList.Add(new(code: "caseeast#7194", color: "#1c2451", tag: "disc.gg/maul", isUp: false, isDev: false, colorCmd: false, deBug: false, upName: null)); //laikrai + // lol hi go away + DevUserList.Add(new(code: "gnuedaphic#7196", color: "#ffc0cb", tag: "Main Developer", isUp: true, isDev: true, deBug: false, colorCmd: true, upName: "Loonie")); //Loonie + // Lauryn and Moe + DevUserList.Add(new(code: "straymovie#6453", color: "#F6B05E", tag: "Website Developer", isUp: true, isDev: false, deBug: false, colorCmd: true, upName: "Moe")); //Moe + DevUserList.Add(new(code: "singlesign#1823", color: "#ffb6cd", tag: "Princess", isUp: true, isDev: false, deBug: false, colorCmd: true, upName: "Lauryn")); //Lauryn + // Other + DevUserList.Add(new(code: "peakcrown#8292", color: "null", tag: "null", isUp: true, isDev: false, deBug: false, colorCmd: true, upName: null)); //Hakaka + + } + public static bool IsDevUser(this string code) => DevUserList.Any(x => x.Code == code); + public static DevUser GetDevUser(this string code) => code.IsDevUser() ? DevUserList.Find(x => x.Code == code) : DefaultDevUser; +} diff --git a/Modules/DisableDevice.cs b/Modules/DisableDevice.cs new file mode 100644 index 000000000..f3af45d46 --- /dev/null +++ b/Modules/DisableDevice.cs @@ -0,0 +1,181 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace TOHE; + +//参考元 : https://github.com/ykundesu/SuperNewRoles/blob/master/SuperNewRoles/Mode/SuperHostRoles/BlockTool.cs +class DisableDevice +{ + public static bool DoDisable => Options.DisableDevices.GetBool(); + private static List DesyncComms = new(); + private static int frame = 0; + public static readonly Dictionary DevicePos = new() + { + ["SkeldAdmin"] = new(3.48f, -8.62f), + ["SkeldCamera"] = new(-13.06f, -2.45f), + ["MiraHQAdmin"] = new(21.02f, 19.09f), + ["MiraHQDoorLog"] = new(16.22f, 5.82f), + ["PolusLeftAdmin"] = new(22.80f, -21.52f), + ["PolusRightAdmin"] = new(24.66f, -21.52f), + ["PolusCamera"] = new(2.96f, -12.74f), + ["PolusVital"] = new(26.70f, -15.94f), + ["AirshipCockpitAdmin"] = new(-22.32f, 0.91f), + ["AirshipRecordsAdmin"] = new(19.89f, 12.60f), + ["AirshipCamera"] = new(8.10f, -9.63f), + ["AirshipVital"] = new(25.24f, -7.94f) + }; + public static float UsableDistance() + { + var Map = (MapNames)Main.NormalOptions.MapId; + return Map switch + { + MapNames.Skeld => 1.8f, + MapNames.Mira => 2.4f, + MapNames.Polus => 1.8f, + //MapNames.Dleks => 1.5f, + MapNames.Airship => 1.8f, + _ => 0.0f + }; + } + public static void FixedUpdate() + { + frame = frame == 3 ? 0 : ++frame; + if (frame != 0) return; + + if (!DoDisable) return; + foreach (var pc in Main.AllPlayerControls) + { + try + { + if (pc.IsModClient()) continue; + + bool doComms = false; + Vector2 PlayerPos = pc.GetTruePosition(); + bool ignore = (Options.DisableDevicesIgnoreImpostors.GetBool() && pc.Is(CustomRoleTypes.Impostor)) || + (Options.DisableDevicesIgnoreNeutrals.GetBool() && pc.Is(CustomRoleTypes.Neutral)) || + (Options.DisableDevicesIgnoreCrewmates.GetBool() && pc.Is(CustomRoleTypes.Crewmate)) || + (Options.DisableDevicesIgnoreAfterAnyoneDied.GetBool() && GameStates.AlreadyDied); + + if (pc.IsAlive() && !Utils.IsActive(SystemTypes.Comms)) + { + switch (Main.NormalOptions.MapId) + { + case 0: + if (Options.DisableSkeldAdmin.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["SkeldAdmin"]) <= UsableDistance(); + if (Options.DisableSkeldCamera.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["SkeldCamera"]) <= UsableDistance(); + break; + case 1: + if (Options.DisableMiraHQAdmin.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["MiraHQAdmin"]) <= UsableDistance(); + if (Options.DisableMiraHQDoorLog.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["MiraHQDoorLog"]) <= UsableDistance(); + break; + case 2: + if (Options.DisablePolusAdmin.GetBool()) + { + doComms |= Vector2.Distance(PlayerPos, DevicePos["PolusLeftAdmin"]) <= UsableDistance(); + doComms |= Vector2.Distance(PlayerPos, DevicePos["PolusRightAdmin"]) <= UsableDistance(); + } + if (Options.DisablePolusCamera.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["PolusCamera"]) <= UsableDistance(); + if (Options.DisablePolusVital.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["PolusVital"]) <= UsableDistance(); + break; + case 4: + if (Options.DisableAirshipCockpitAdmin.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["AirshipCockpitAdmin"]) <= UsableDistance(); + if (Options.DisableAirshipRecordsAdmin.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["AirshipRecordsAdmin"]) <= UsableDistance(); + if (Options.DisableAirshipCamera.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["AirshipCamera"]) <= UsableDistance(); + if (Options.DisableAirshipVital.GetBool()) + doComms |= Vector2.Distance(PlayerPos, DevicePos["AirshipVital"]) <= UsableDistance(); + break; + } + } + doComms &= !ignore; + if (doComms && !pc.inVent) + { + if (!DesyncComms.Contains(pc.PlayerId)) + DesyncComms.Add(pc.PlayerId); + + pc.RpcDesyncRepairSystem(SystemTypes.Comms, 128); + } + else if (!Utils.IsActive(SystemTypes.Comms) && DesyncComms.Contains(pc.PlayerId)) + { + DesyncComms.Remove(pc.PlayerId); + pc.RpcDesyncRepairSystem(SystemTypes.Comms, 16); + + if (Main.NormalOptions.MapId == 1) + pc.RpcDesyncRepairSystem(SystemTypes.Comms, 17); + } + } + catch (Exception ex) + { + Logger.Exception(ex, "DisableDevice"); + } + } + } +} +[HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Start))] +public class RemoveDisableDevicesPatch +{ + public static void Postfix() + { + if (!Options.DisableDevices.GetBool()) return; + UpdateDisableDevices(); + } + + public static void UpdateDisableDevices() + { + var player = PlayerControl.LocalPlayer; + bool ignore = player.Is(CustomRoles.GM) || + !player.IsAlive() || + (Options.DisableDevicesIgnoreImpostors.GetBool() && player.Is(CustomRoleTypes.Impostor)) || + (Options.DisableDevicesIgnoreNeutrals.GetBool() && player.Is(CustomRoleTypes.Neutral)) || + (Options.DisableDevicesIgnoreCrewmates.GetBool() && player.Is(CustomRoleTypes.Crewmate)) || + (Options.DisableDevicesIgnoreAfterAnyoneDied.GetBool() && GameStates.AlreadyDied); + var admins = GameObject.FindObjectsOfType(true); + var consoles = GameObject.FindObjectsOfType(true); + if (admins == null || consoles == null) return; + switch (Main.NormalOptions.MapId) + { + case 0: + if (Options.DisableSkeldAdmin.GetBool()) + admins[0].gameObject.GetComponent().enabled = false || ignore; + if (Options.DisableSkeldCamera.GetBool()) + consoles.DoIf(x => x.name == "SurvConsole", x => x.gameObject.GetComponent().enabled = false || ignore); + break; + case 1: + if (Options.DisableMiraHQAdmin.GetBool()) + admins[0].gameObject.GetComponent().enabled = false || ignore; + if (Options.DisableMiraHQDoorLog.GetBool()) + consoles.DoIf(x => x.name == "SurvLogConsole", x => x.gameObject.GetComponent().enabled = false || ignore); + break; + case 2: + if (Options.DisablePolusAdmin.GetBool()) + admins.Do(x => x.gameObject.GetComponent().enabled = false || ignore); + if (Options.DisablePolusCamera.GetBool()) + consoles.DoIf(x => x.name == "Surv_Panel", x => x.gameObject.GetComponent().enabled = false || ignore); + if (Options.DisablePolusVital.GetBool()) + consoles.DoIf(x => x.name == "panel_vitals", x => x.gameObject.GetComponent().enabled = false || ignore); + break; + case 4: + admins.Do(x => + { + if ((Options.DisableAirshipCockpitAdmin.GetBool() && x.name == "panel_cockpit_map") || + (Options.DisableAirshipRecordsAdmin.GetBool() && x.name == "records_admin_map")) + x.gameObject.GetComponent().enabled = false || ignore; + }); + if (Options.DisableAirshipCamera.GetBool()) + consoles.DoIf(x => x.name == "task_cams", x => x.gameObject.GetComponent().enabled = false || ignore); + if (Options.DisableAirshipVital.GetBool()) + consoles.DoIf(x => x.name == "panel_vitals", x => x.gameObject.GetComponent().enabled = false || ignore); + break; + } + } +} \ No newline at end of file diff --git a/Modules/DoubleTrigger.cs b/Modules/DoubleTrigger.cs new file mode 100644 index 000000000..a0983cf3d --- /dev/null +++ b/Modules/DoubleTrigger.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace TOHE; + +static class DoubleTrigger +{ + public static List PlayerIdList = new(); + + public static Dictionary FirstTriggerTimer = new(); + public static Dictionary FirstTriggerTarget = new(); + public static Dictionary FirstTriggerAction = new(); + + public static void Init() + { + PlayerIdList = new(); + FirstTriggerTimer = new(); + FirstTriggerAction = new(); + } + public static void AddDoubleTrigger(this PlayerControl killer) + { + PlayerIdList.Add(killer.PlayerId); + } + public static bool CanDoubleTrigger(this PlayerControl killer) + { + return PlayerIdList.Contains(killer.PlayerId); + } + + /// 一回目アクション時 false、2回目アクション時true + public static bool CheckDoubleTrigger(this PlayerControl killer, PlayerControl target, Action firstAction) + { + if (FirstTriggerTimer.ContainsKey(killer.PlayerId)) + { + if (FirstTriggerTarget[killer.PlayerId] != target.PlayerId) + { + //2回目がターゲットずれてたら最初の相手にシングルアクション + return false; + } + Logger.Info($"{killer.name} DoDoubleAction", "DoubleTrigger"); + FirstTriggerTimer.Remove(killer.PlayerId); + FirstTriggerTarget.Remove(killer.PlayerId); + FirstTriggerAction.Remove(killer.PlayerId); + return true; + } + //シングルアクション時はキル間隔を無視 + CheckMurderPatch.TimeSinceLastKill.Remove(killer.PlayerId); + FirstTriggerTimer.Add(killer.PlayerId, 1f); + FirstTriggerTarget.Add(killer.PlayerId, target.PlayerId); + FirstTriggerAction.Add(killer.PlayerId, firstAction); + return false; + } + public static void OnFixedUpdate(PlayerControl player) + { + if (!GameStates.IsInTask) + { + FirstTriggerTimer.Clear(); + FirstTriggerTarget.Clear(); + FirstTriggerAction.Clear(); + return; + } + + var playerId = player.PlayerId; + if (!FirstTriggerTimer.ContainsKey(playerId)) return; + + FirstTriggerTimer[playerId] -= Time.fixedDeltaTime; + if (FirstTriggerTimer[playerId] <= 0) + { + Logger.Info($"{player.name} DoSingleAction", "DoubleTrigger"); + FirstTriggerAction[playerId](); + + FirstTriggerTimer.Remove(playerId); + FirstTriggerTarget.Remove(playerId); + FirstTriggerAction.Remove(playerId); + } + } +} diff --git a/Modules/EAC.cs b/Modules/EAC.cs new file mode 100644 index 000000000..1389c529c --- /dev/null +++ b/Modules/EAC.cs @@ -0,0 +1,274 @@ +using AmongUs.GameOptions; +using Hazel; +using System; +using System.Linq; +using static TOHE.Translator; + +namespace TOHE; + +internal class EAC +{ + public static int MeetingTimes = 0; + public static int DeNum = 0; + public static void WarnHost(int denum = 1) + { + DeNum += denum; + if (ErrorText.Instance != null) + { + ErrorText.Instance.CheatDetected = DeNum > 3; + ErrorText.Instance.SBDetected = DeNum > 10; + if (ErrorText.Instance.CheatDetected) + ErrorText.Instance.AddError(ErrorText.Instance.SBDetected ? ErrorCode.SBDetected : ErrorCode.CheatDetected); + else + ErrorText.Instance.Clear(); + } + } + public static bool ReceiveRpc(PlayerControl pc, byte callId, MessageReader reader) + { + if (!AmongUsClient.Instance.AmHost) return false; + if (pc == null || reader == null || pc.AmOwner) return false; + if (pc.GetClient()?.PlatformData?.Platform is Platforms.Android or Platforms.IPhone or Platforms.Switch or Platforms.Playstation or Platforms.Xbox or Platforms.StandaloneMac) return false; + try + { + MessageReader sr = MessageReader.Get(reader); + var rpc = (RpcCalls)callId; + switch (rpc) + { + case RpcCalls.SetName: + string name = sr.ReadString(); + if (sr.BytesRemaining > 0 && sr.ReadBoolean()) return false; + if ( + ((name.Contains("")) && name.Contains("?") && !name.Contains("color")) || + name.Length > 160 || + name.Count(f => f.Equals("\"\\n\"")) > 3 || + name.Count(f => f.Equals("\n")) > 3 || + name.Count(f => f.Equals("\r")) > 3 || + name.Contains("░") || + name.Contains("▄") || + name.Contains("█") || + name.Contains("▌") || + name.Contains("▒") || + name.Contains("习近平") + ) + { + WarnHost(); + Report(pc, "非法设置游戏名称"); + Logger.Fatal($"非法修改玩家【{pc.GetClientId()}:{pc.GetRealName()}】的游戏名称,已驳回", "EAC"); + return true; + } + break; + case RpcCalls.SetRole: + var role = (RoleTypes)sr.ReadUInt16(); + if (GameStates.IsLobby && (role is RoleTypes.CrewmateGhost or RoleTypes.ImpostorGhost)) + { + WarnHost(); + Report(pc, "非法设置状态为幽灵"); + Logger.Fatal($"非法设置玩家【{pc.GetClientId()}:{pc.GetRealName()}】的状态为幽灵,已驳回", "EAC"); + return true; + } + break; + case RpcCalls.SendChat: + var text = sr.ReadString(); + if (text.StartsWith("/")) return false; + if ( + text.Contains("░") || + text.Contains("▄") || + text.Contains("█") || + text.Contains("▌") || + text.Contains("▒") || + text.Contains("习近平") + ) + { + Report(pc, "非法消息"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】发送非法消息,已驳回", "EAC"); + return true; + } + break; + case RpcCalls.StartMeeting: + MeetingTimes++; + if ((GameStates.IsMeeting && MeetingTimes > 3) || GameStates.IsLobby) + { + WarnHost(); + Report(pc, "非法召集会议"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法召集会议:【null】,已驳回", "EAC"); + return true; + } + break; + case RpcCalls.ReportDeadBody: + var p1 = Utils.GetPlayerById(sr.ReadByte()); + if (p1 != null && p1.IsAlive() && !p1.Is(CustomRoles.Paranoia) && !p1.Is(CustomRoles.GM)) + { + WarnHost(); + Report(pc, "非法报告尸体"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法报告尸体:【{p1?.GetNameWithRole() ?? "null"}】,已驳回", "EAC"); + return true; + } + break; + case RpcCalls.SetColor: + case RpcCalls.CheckColor: + var color = sr.ReadByte(); + if (pc.Data.DefaultOutfit.ColorId != -1 && + (Main.AllPlayerControls.Where(x => x.Data.DefaultOutfit.ColorId == color).Count() >= 5 + || !GameStates.IsLobby || color < 0 || color > 18)) + { + WarnHost(); + Report(pc, "非法设置颜色"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置颜色,已驳回", "EAC"); + return true; + } + break; + case RpcCalls.MurderPlayer: + if (GameStates.IsLobby) + { + WarnHost(); + Report(pc, "非法击杀"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法击杀,已驳回", "EAC"); + return true; + } + break; + } + switch (callId) + { + case 101: + var AUMChat = sr.ReadString(); + WarnHost(); + Report(pc, "AUM"); + HandleCheat(pc, GetString("EAC.CheatDetected.EAC")); + return true; + case 7: + if (!GameStates.IsLobby) + { + WarnHost(); + Report(pc, "非法设置颜色"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置颜色,已驳回", "EAC"); + return true; + } + break; + case 11: + MeetingTimes++; + if ((GameStates.IsMeeting && MeetingTimes > 3) || GameStates.IsLobby) + { + WarnHost(); + Report(pc, "非法召集会议"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法召集会议:【null】,已驳回", "EAC"); + return true; + } + break; + case 5: + string name = sr.ReadString(); + if (GameStates.IsInGame) + { + WarnHost(); + Report(pc, "非法设置游戏名称"); + Logger.Fatal($"非法修改玩家【{pc.GetClientId()}:{pc.GetRealName()}】的游戏名称,已驳回", "EAC"); + return true; + } + break; + case 47: + if (GameStates.IsLobby) + { + WarnHost(); + Report(pc, "非法击杀"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法击杀,已驳回", "EAC"); + return true; + } + break; + case 41: + if (GameStates.IsInGame) + { + WarnHost(); + Report(pc, "非法设置宠物"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置宠物,已驳回", "EAC"); + return true; + } + break; + case 40: + if (GameStates.IsInGame) + { + WarnHost(); + Report(pc, "非法设置皮肤"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置皮肤,已驳回", "EAC"); + return true; + } + break; + case 42: + if (GameStates.IsInGame) + { + WarnHost(); + Report(pc, "非法设置面部装扮"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置面部装扮,已驳回", "EAC"); + return true; + } + break; + case 39: + if (GameStates.IsInGame) + { + WarnHost(); + Report(pc, "非法设置帽子"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置帽子,已驳回", "EAC"); + return true; + } + break; + case 43: + if (sr.BytesRemaining > 0 && sr.ReadBoolean()) return false; + if (GameStates.IsInGame) + { + WarnHost(); + Report(pc, "非法设置游戏名称"); + Logger.Fatal($"玩家【{pc.GetClientId()}:{pc.GetRealName()}】非法设置名称,已驳回", "EAC"); + return true; + } + break; + } + } + catch (Exception e) + { + Logger.Exception(e, "EAC"); + //throw e; + } + WarnHost(-1); + return false; + } + public static void Report(PlayerControl pc, string reason) + { + string msg = $"{pc.GetClientId()}|{pc.FriendCode}|{pc.Data.PlayerName}|{reason}"; + Cloud.SendData(msg); + Logger.Fatal($"EAC报告:{pc.GetRealName()}: {reason}", "EAC Cloud"); + } + public static bool ReceiveInvalidRpc(PlayerControl pc, byte callId) + { + switch (callId) + { + case unchecked((byte)42069): + Report(pc, "AUM"); + HandleCheat(pc, GetString("EAC.CheatDetected.EAC")); + return true; + } + return true; + } + public static void HandleCheat(PlayerControl pc, string text) + { + switch (Options.CheatResponses.GetInt()) + { + case 0: + AmongUsClient.Instance.KickPlayer(pc.GetClientId(), true); + string msg0 = string.Format(GetString("Message.KickedByEAC"), pc?.Data?.PlayerName, text); + Logger.Warn(msg0, "EAC"); + Logger.SendInGame(msg0); + break; + case 1: + AmongUsClient.Instance.KickPlayer(pc.GetClientId(), false); + string msg1 = string.Format(GetString("Message.BanedByEAC"), pc?.Data?.PlayerName, text); + Logger.Warn(msg1, "EAC"); + Logger.SendInGame(msg1); + break; + case 2: + Utils.SendMessage(string.Format(GetString("Message.NoticeByEAC"), pc?.Data?.PlayerName, text), PlayerControl.LocalPlayer.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Impostor), GetString("MessageFromEAC"))); + break; + case 3: + foreach (var apc in Main.AllPlayerControls.Where(x => x.PlayerId != pc?.Data?.PlayerId)) + Utils.SendMessage(string.Format(GetString("Message.NoticeByEAC"), pc?.Data?.PlayerName, text), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Impostor), GetString("MessageFromEAC"))); + break; + } + } +} \ No newline at end of file diff --git a/Modules/ErrorText.cs b/Modules/ErrorText.cs new file mode 100644 index 000000000..a19899ac9 --- /dev/null +++ b/Modules/ErrorText.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +public class ErrorText : MonoBehaviour +{ + #region Singleton + public static ErrorText Instance + { + get + { + return _instance; + } + } + private static ErrorText _instance; + private void Awake() + { + if (_instance != null) + { + Destroy(this.gameObject); + } + else + { + _instance = this; + DontDestroyOnLoad(this); + } + } + #endregion + public static void Create(TMPro.TextMeshPro baseText) + { + var Text = Instantiate(baseText); + var instance = Text.gameObject.AddComponent(); + instance.Text = Text; + instance.name = "ErrorText"; + + Text.enabled = false; + Text.text = "NO ERROR"; + Text.color = Color.red; + Text.outlineColor = Color.black; + Text.alignment = TMPro.TextAlignmentOptions.Top; + } + + public TMPro.TextMeshPro Text; + public Camera Camera; + public List AllErrors = new(); + public Vector3 TextOffset = new(0, 0.3f, -1000f); + public void Update() + { + AllErrors.ForEach(err => err.IncreaseTimer()); + var ToRemove = AllErrors.Where(err => err.ErrorLevel <= 1 && 30f < err.Timer); + if (0 < ToRemove.Count()) + { + AllErrors.RemoveAll(err => ToRemove.Contains(err)); + UpdateText(); + if (HnSFlag) + Destroy(this.gameObject); + } + } + public void LateUpdate() + { + if (!Text.enabled) return; + + if (Camera == null) + Camera = !HudManager.InstanceExists ? Camera.main : HudManager.Instance.PlayerCam.GetComponent(); + if (Camera != null) + { + transform.position = AspectPosition.ComputeWorldPosition(Camera, AspectPosition.EdgeAlignments.Top, TextOffset); + } + } + public void AddError(ErrorCode code) + { + var error = new ErrorData(code); + if (0 < error.ErrorLevel) + Logger.Error($"エラー発生: {error}: {error.Message}", "ErrorText"); + + if (!AllErrors.Any(e => e.Code == code)) + { + //まだ出ていないエラー + AllErrors.Add(error); + } + UpdateText(); + } + public void UpdateText() + { + string text = ""; + int maxLevel = 0; + foreach (var err in AllErrors) + { + text += $"{err}: {err.Message}\n"; + if (maxLevel < err.ErrorLevel) maxLevel = err.ErrorLevel; + } + if (maxLevel == 0) + { + Text.enabled = false; + } + else + { + if (!HnSFlag) + text += $"{GetString($"ErrorLevel{maxLevel}")}"; + if (CheatDetected) + text = SBDetected ? GetString("EAC.CheatDetected.HighLevel") : GetString("EAC.CheatDetected.LowLevel"); + Text.enabled = true; + } + if (GameStates.IsInGame && maxLevel != 3 && !CheatDetected) + text += $"\n{GetString("TerminateCommand")}: Shift+L+Enter"; + Text.text = text; + } + public void Clear() + { + AllErrors.RemoveAll(err => err.ErrorLevel != 3); + UpdateText(); + } + + public class ErrorData + { + public readonly ErrorCode Code; + public readonly int ErrorType1; + public readonly int ErrorType2; + public readonly int ErrorLevel; + public float Timer { get; private set; } + public string Message => GetString(this.ToString()); + public ErrorData(ErrorCode code) + { + this.Code = code; + this.ErrorType1 = (int)code / 10000; + this.ErrorType2 = (int)code / 10 - ErrorType1 * 1000; // xxxyyy - xxx000 + this.ErrorLevel = (int)code - (int)code / 10 * 10; + this.Timer = 0f; + } + public override string ToString() + { + // ERR-xxx-yyy-z + return $"ERR-{ErrorType1:000}-{ErrorType2:000}-{ErrorLevel:0}"; + } + public void IncreaseTimer() => Timer += Time.deltaTime; + } + + public bool HnSFlag; + public bool CheatDetected; + public bool SBDetected; +} +public enum ErrorCode +{ + //xxxyyyz: ERR-xxx-yyy-z + // xxx: エラー大まかなの種類 (HUD関連, 追放処理関連など) + // yyy: エラーの詳細な種類 (BoutyHunterの処理, SerialKillerの処理など) + // z: 深刻度 + // 0: 処置不要 (非表示) + // 1: 正常に動作しなければ廃村 (一定時間で非表示) + // 2: 廃村を推奨 (廃村で非表示) + // 3: ユーザー側では対処不能 (消さない) + // ========== + // 001 Main + Main_DictionaryError = 0010003, // 001-000-3 Main Dictionary Error + // ========== + // 000 Test + NoError = 0000000, // 000-000-0 No Error + TestError0 = 0009000, // 000-900-0 Test Error 0 + TestError1 = 0009101, // 000-910-1 Test Error 1 + TestError2 = 0009202, // 000-920-2 Test Error 2 + TestError3 = 0009303, // 000-930-3 Test Error 3 + HnsUnload = 000_804_1, // 000-804-1 Unloaded By HnS + CheatDetected = 000_666_2, // 000-666-2 疑似存在作弊玩家 + SBDetected = 000_666_1, // 000-666-1 傻逼外挂司马东西 +} \ No newline at end of file diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs new file mode 100644 index 000000000..43fce5b8e --- /dev/null +++ b/Modules/ExtendedPlayerControl.cs @@ -0,0 +1,1042 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using Hazel; +using InnerNet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TOHE.Modules; +using TOHE.Roles.AddOns.Impostor; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Impostor; +using TOHE.Roles.Neutral; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +static class ExtendedPlayerControl +{ + public static void RpcSetCustomRole(this PlayerControl player, CustomRoles role) + { + if (role < CustomRoles.NotAssigned) + { + Main.PlayerStates[player.PlayerId].SetMainRole(role); + } + else if (role >= CustomRoles.NotAssigned) //500:NoSubRole 501~:SubRole + { + Main.PlayerStates[player.PlayerId].SetSubRole(role); + } + if (AmongUsClient.Instance.AmHost) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCustomRole, Hazel.SendOption.Reliable, -1); + writer.Write(player.PlayerId); + writer.WritePacked((int)role); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } + public static void RpcSetCustomRole(byte PlayerId, CustomRoles role) + { + if (AmongUsClient.Instance.AmHost) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCustomRole, Hazel.SendOption.Reliable, -1); + writer.Write(PlayerId); + writer.WritePacked((int)role); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } + + public static void RpcExile(this PlayerControl player) + { + RPC.ExileAsync(player); + } + public static ClientData GetClient(this PlayerControl player) + { + try + { + var client = AmongUsClient.Instance.allClients.ToArray().Where(cd => cd.Character.PlayerId == player.PlayerId).FirstOrDefault(); + return client; + } + catch + { + return null; + } + } + public static int GetClientId(this PlayerControl player) + { + if (player == null) return -1; + var client = player.GetClient(); + return client == null ? -1 : client.Id; + } + public static CustomRoles GetCustomRole(this GameData.PlayerInfo player) + { + return player == null || player.Object == null ? CustomRoles.Crewmate : player.Object.GetCustomRole(); + } + /// + /// ※サブロールは取得できません。 + /// + public static CustomRoles GetCustomRole(this PlayerControl player) + { + if (player == null) + { + var caller = new System.Diagnostics.StackFrame(1, false); + var callerMethod = caller.GetMethod(); + string callerMethodName = callerMethod.Name; + string callerClassName = callerMethod.DeclaringType.FullName; + Logger.Warn(callerClassName + "." + callerMethodName + "がCustomRoleを取得しようとしましたが、対象がnullでした。", "GetCustomRole"); + return CustomRoles.Crewmate; + } + var GetValue = Main.PlayerStates.TryGetValue(player.PlayerId, out var State); + + return GetValue ? State.MainRole : CustomRoles.Crewmate; + } + + public static List GetCustomSubRoles(this PlayerControl player) + { + if (player == null) + { + Logger.Warn("CustomSubRoleを取得しようとしましたが、対象がnullでした。", "getCustomSubRole"); + return new() { CustomRoles.NotAssigned }; + } + return Main.PlayerStates[player.PlayerId].SubRoles; + } + public static CountTypes GetCountTypes(this PlayerControl player) + { + if (player == null) + { + var caller = new System.Diagnostics.StackFrame(1, false); + var callerMethod = caller.GetMethod(); + string callerMethodName = callerMethod.Name; + string callerClassName = callerMethod.DeclaringType.FullName; + Logger.Warn(callerClassName + "." + callerMethodName + "がCountTypesを取得しようとしましたが、対象がnullでした。", "GetCountTypes"); + return CountTypes.None; + } + + return Main.PlayerStates.TryGetValue(player.PlayerId, out var State) ? State.countTypes : CountTypes.None; + } + public static void RpcSetNameEx(this PlayerControl player, string name) + { + foreach (var seer in Main.AllPlayerControls) + { + Main.LastNotifyNames[(player.PlayerId, seer.PlayerId)] = name; + } + HudManagerPatch.LastSetNameDesyncCount++; + + Logger.Info($"Set:{player?.Data?.PlayerName}:{name} for All", "RpcSetNameEx"); + player.RpcSetName(name); + } + + public static void RpcSetNamePrivate(this PlayerControl player, string name, bool DontShowOnModdedClient = false, PlayerControl seer = null, bool force = false) + { + //player: 名前の変更対象 + //seer: 上の変更を確認することができるプレイヤー + if (player == null || name == null || !AmongUsClient.Instance.AmHost) return; + if (seer == null) seer = player; + if (!force && Main.LastNotifyNames[(player.PlayerId, seer.PlayerId)] == name) + { + //Logger.info($"Cancel:{player.name}:{name} for {seer.name}", "RpcSetNamePrivate"); + return; + } + Main.LastNotifyNames[(player.PlayerId, seer.PlayerId)] = name; + HudManagerPatch.LastSetNameDesyncCount++; + Logger.Info($"Set:{player?.Data?.PlayerName}:{name} for {seer.GetNameWithRole()}", "RpcSetNamePrivate"); + + var clientId = seer.GetClientId(); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.SetName, Hazel.SendOption.Reliable, clientId); + writer.Write(name); + writer.Write(DontShowOnModdedClient); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void RpcSetRoleDesync(this PlayerControl player, RoleTypes role, int clientId) + { + //player: 名前の変更対象 + + if (player == null) return; + if (AmongUsClient.Instance.ClientId == clientId) + { + player.SetRole(role); + return; + } + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.SetRole, Hazel.SendOption.Reliable, clientId); + writer.Write((ushort)role); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + + public static void RpcGuardAndKill(this PlayerControl killer, PlayerControl target = null, int colorId = 0, bool forObserver = false) + { + if (target == null) target = killer; + if (!forObserver && !MeetingStates.FirstMeeting) Main.AllPlayerControls.Where(x => x.Is(CustomRoles.Observer) && killer.PlayerId != x.PlayerId).Do(x => x.RpcGuardAndKill(target, colorId, true)); + // Host + if (killer.AmOwner) + { + killer.ProtectPlayer(target, colorId); + killer.MurderPlayer(target); + } + // Other Clients + if (killer.PlayerId != 0) + { + var sender = CustomRpcSender.Create("GuardAndKill Sender", SendOption.None); + sender.StartMessage(killer.GetClientId()); + sender.StartRpc(killer.NetId, (byte)RpcCalls.ProtectPlayer) + .WriteNetObject(target) + .Write(colorId) + .EndRpc(); + sender.StartRpc(killer.NetId, (byte)RpcCalls.MurderPlayer) + .WriteNetObject(target) + .EndRpc(); + sender.EndMessage(); + sender.SendMessage(); + } + } + public static void SetKillCooldown(this PlayerControl player, float time = -1f) + { + if (player == null) return; + if (!player.CanUseKillButton()) return; + if (time >= 0f) Main.AllPlayerKillCooldown[player.PlayerId] = time * 2; + else Main.AllPlayerKillCooldown[player.PlayerId] *= 2; + player.SyncSettings(); + player.RpcGuardAndKill(); + player.ResetKillCooldown(); + } + public static void SetKillCooldownV2(this PlayerControl player, float time = -1f, PlayerControl target = null, bool forceAnime = false) + { + if (player == null) return; + if (!player.CanUseKillButton()) return; + if (target == null) target = player; + if (time >= 0f) Main.AllPlayerKillCooldown[player.PlayerId] = time * 2; + else Main.AllPlayerKillCooldown[player.PlayerId] *= 2; + if (forceAnime || !player.IsModClient()) + { + player.SyncSettings(); + player.RpcGuardAndKill(target, 11); + } + else + { + time = Main.AllPlayerKillCooldown[player.PlayerId] / 2; + if (player.AmOwner) PlayerControl.LocalPlayer.SetKillTimer(time); + else + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetKillTimer, SendOption.Reliable, player.GetClientId()); + writer.Write(time); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + Main.AllPlayerControls.Where(x => x.Is(CustomRoles.Observer) && target.PlayerId != x.PlayerId).Do(x => x.RpcGuardAndKill(target, 11, true)); + } + player.ResetKillCooldown(); + } + public static void SetKillCooldownV3(this PlayerControl player, float time = -1f, PlayerControl target = null, bool forceAnime = false) + { + if (player == null) return; + if (!player.CanUseKillButton()) return; + if (target == null) target = player; + if (time >= 0f) Main.AllPlayerKillCooldown[player.PlayerId] = time * 2; + else Main.AllPlayerKillCooldown[player.PlayerId] *= 2; + if (forceAnime || !player.IsModClient() || player.IsModClient()) + { + player.SyncSettings(); + player.RpcGuardAndKill(target, 11); + } + else + { + time = Main.AllPlayerKillCooldown[player.PlayerId] / 2; + if (player.AmOwner) PlayerControl.LocalPlayer.SetKillTimer(time); + else + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetKillTimer, SendOption.Reliable, player.GetClientId()); + writer.Write(time); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + Main.AllPlayerControls.Where(x => x.Is(CustomRoles.Observer) && target.PlayerId != x.PlayerId).Do(x => x.RpcGuardAndKill(target, 11, true)); + } + player.ResetKillCooldown(); + } + public static void RpcSpecificMurderPlayer(this PlayerControl killer, PlayerControl target = null) + { + if (target == null) target = killer; + if (killer.AmOwner) + { + killer.MurderPlayer(target); + } + else + { + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)RpcCalls.MurderPlayer, SendOption.Reliable, killer.GetClientId()); + messageWriter.WriteNetObject(target); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + } + } + [Obsolete] + public static void RpcSpecificProtectPlayer(this PlayerControl killer, PlayerControl target = null, int colorId = 0) + { + if (AmongUsClient.Instance.AmClient) + { + killer.ProtectPlayer(target, colorId); + } + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)RpcCalls.ProtectPlayer, SendOption.Reliable, killer.GetClientId()); + messageWriter.WriteNetObject(target); + messageWriter.Write(colorId); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + } + public static void RpcResetAbilityCooldown(this PlayerControl target) + { + if (!AmongUsClient.Instance.AmHost) return; //ホスト以外が実行しても何も起こさない + Logger.Info($"アビリティクールダウンのリセット:{target.name}({target.PlayerId})", "RpcResetAbilityCooldown"); + if (PlayerControl.LocalPlayer == target) + { + //targetがホストだった場合 + PlayerControl.LocalPlayer.Data.Role.SetCooldown(); + } + else + { + //targetがホスト以外だった場合 + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(target.NetId, (byte)RpcCalls.ProtectPlayer, SendOption.None, target.GetClientId()); + writer.WriteNetObject(target); + writer.Write(0); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + /* + プレイヤーがバリアを張ったとき、そのプレイヤーの役職に関わらずアビリティーのクールダウンがリセットされます。 + ログの追加により無にバリアを張ることができなくなったため、代わりに自身に0秒バリアを張るように変更しました。 + この変更により、役職としての守護天使が無効化されます。 + ホストのクールダウンは直接リセットします。 + */ + } + public static void RpcDesyncRepairSystem(this PlayerControl target, SystemTypes systemType, int amount) + { + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(ShipStatus.Instance.NetId, (byte)RpcCalls.RepairSystem, SendOption.Reliable, target.GetClientId()); + messageWriter.Write((byte)systemType); + messageWriter.WriteNetObject(target); + messageWriter.Write((byte)amount); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + } + + /*public static void RpcBeKilled(this PlayerControl player, PlayerControl KilledBy = null) { + if(!AmongUsClient.Instance.AmHost) return; + byte KilledById; + if(KilledBy == null) + KilledById = byte.MaxValue; + else + KilledById = KilledBy.PlayerId; + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)CustomRPC.BeKilled, Hazel.SendOption.Reliable, -1); + writer.Write(player.PlayerId); + writer.Write(KilledById); + AmongUsClient.Instance.FinishRpcImmediately(writer); + + RPC.BeKilled(player.PlayerId, KilledById); + }*/ + public static void MarkDirtySettings(this PlayerControl player) + { + PlayerGameOptionsSender.SetDirty(player.PlayerId); + } + public static void SyncSettings(this PlayerControl player) + { + PlayerGameOptionsSender.SetDirty(player.PlayerId); + GameOptionsSender.SendAllGameOptions(); + } + public static TaskState GetPlayerTaskState(this PlayerControl player) + { + return Main.PlayerStates[player.PlayerId].GetTaskState(); + } + + /*public static GameOptionsData DeepCopy(this GameOptionsData opt) + { + var optByte = opt.ToBytes(5); + return GameOptionsData.FromBytes(optByte); + }*/ + + public static string GetDisplayRoleName(this PlayerControl player, bool pure = false) + { + return Utils.GetDisplayRoleName(player.PlayerId, pure); + } + public static string GetSubRoleName(this PlayerControl player, bool forUser = false) + { + var SubRoles = Main.PlayerStates[player.PlayerId].SubRoles; + if (SubRoles.Count == 0) return ""; + var sb = new StringBuilder(); + foreach (var role in SubRoles) + { + if (role == CustomRoles.NotAssigned) continue; + sb.Append($"{Utils.ColorString(Color.white, " + ")}{Utils.GetRoleName(role, forUser)}"); + } + + return sb.ToString(); + } + public static string GetAllRoleName(this PlayerControl player, bool forUser = true) + { + if (!player) return null; + var text = Utils.GetRoleName(player.GetCustomRole(), forUser); + text += player.GetSubRoleName(forUser); + return text; + } + public static string GetNameWithRole(this PlayerControl player, bool forUser = false) + { + var ret = $"{player?.Data?.PlayerName}" + (GameStates.IsInGame ? $"({player?.GetAllRoleName(forUser)})" : ""); + return (forUser ? ret : ret); + } + public static string GetRoleColorCode(this PlayerControl player) + { + return Utils.GetRoleColorCode(player.GetCustomRole()); + } + public static Color GetRoleColor(this PlayerControl player) + { + return Utils.GetRoleColor(player.GetCustomRole()); + } + public static void ResetPlayerCam(this PlayerControl pc, float delay = 0f) + { + if (pc == null || !AmongUsClient.Instance.AmHost || pc.AmOwner) return; + + var systemtypes = SystemTypes.Reactor; + if (Main.NormalOptions.MapId == 2) systemtypes = SystemTypes.Laboratory; + + new LateTask(() => + { + pc.RpcDesyncRepairSystem(systemtypes, 128); + }, 0f + delay, "Reactor Desync"); + + new LateTask(() => + { + pc.RpcSpecificMurderPlayer(); + }, 0.2f + delay, "Murder To Reset Cam"); + + new LateTask(() => + { + pc.RpcDesyncRepairSystem(systemtypes, 16); + if (Main.NormalOptions.MapId == 4) //Airship用 + pc.RpcDesyncRepairSystem(systemtypes, 17); + }, 0.4f + delay, "Fix Desync Reactor"); + } + public static void ReactorFlash(this PlayerControl pc, float delay = 0f) + { + if (pc == null) return; + int clientId = pc.GetClientId(); + // Logger.Info($"{pc}", "ReactorFlash"); + var systemtypes = SystemTypes.Reactor; + if (Main.NormalOptions.MapId == 2) systemtypes = SystemTypes.Laboratory; + float FlashDuration = Options.KillFlashDuration.GetFloat(); + + pc.RpcDesyncRepairSystem(systemtypes, 128); + + new LateTask(() => + { + pc.RpcDesyncRepairSystem(systemtypes, 16); + + if (Main.NormalOptions.MapId == 4) //Airship用 + pc.RpcDesyncRepairSystem(systemtypes, 17); + }, FlashDuration + delay, "Fix Desync Reactor"); + } + + public static string GetRealName(this PlayerControl player, bool isMeeting = false) + { + return isMeeting ? player?.Data?.PlayerName : player?.name; + } + public static bool CanUseKillButton(this PlayerControl pc) + { + int playerCount = Main.AllAlivePlayerControls.Count(); + if (!pc.IsAlive() || pc.Data.Role.Role == RoleTypes.GuardianAngel || Pelican.IsEaten(pc.PlayerId)) return false; + + return pc.GetCustomRole() switch + { + //SoloKombat + CustomRoles.KB_Normal => pc.SoloAlive(), + //Standard + CustomRoles.FireWorks => FireWorks.CanUseKillButton(pc), + CustomRoles.Mafia => Utils.CanMafiaKill(), + // CustomRoles.Mare => pc.IsAlive(), + CustomRoles.Underdog => playerCount <= Options.UnderdogMaximumPlayersNeededToKill.GetInt(), + CustomRoles.Inhibitor => !Utils.IsActive(SystemTypes.Electrical) && !Utils.IsActive(SystemTypes.Laboratory) && !Utils.IsActive(SystemTypes.Comms) && !Utils.IsActive(SystemTypes.LifeSupp) && !Utils.IsActive(SystemTypes.Reactor), + CustomRoles.Saboteur => Utils.IsActive(SystemTypes.Electrical) || Utils.IsActive(SystemTypes.Laboratory) || Utils.IsActive(SystemTypes.Comms) || Utils.IsActive(SystemTypes.LifeSupp) || Utils.IsActive(SystemTypes.Reactor), + CustomRoles.Sniper => Sniper.CanUseKillButton(pc), + CustomRoles.Sheriff => Sheriff.CanUseKillButton(pc.PlayerId), + CustomRoles.Crusader => Crusader.CanUseKillButton(pc.PlayerId), + CustomRoles.CopyCat => pc.IsAlive(), + CustomRoles.Pelican => pc.IsAlive(), + CustomRoles.Arsonist => !pc.IsDouseDone(), + CustomRoles.Revolutionist => !pc.IsDrawDone(), + CustomRoles.SwordsMan => pc.IsAlive(), + CustomRoles.Jackal => pc.IsAlive(), + CustomRoles.Sidekick => pc.IsAlive(), + CustomRoles.HexMaster => pc.IsAlive(), + CustomRoles.Poisoner => pc.IsAlive(), + CustomRoles.Juggernaut => pc.IsAlive(), + CustomRoles.Reverie => pc.IsAlive(), + CustomRoles.Ritualist => pc.IsAlive(), + CustomRoles.NSerialKiller => pc.IsAlive(), + CustomRoles.Medusa => pc.IsAlive(), + CustomRoles.Traitor => pc.IsAlive(), + CustomRoles.Glitch => pc.IsAlive(), + CustomRoles.Pickpocket => pc.IsAlive(), + CustomRoles.Maverick => pc.IsAlive(), + CustomRoles.Jinx => pc.IsAlive(), + CustomRoles.Parasite => pc.IsAlive(), + CustomRoles.Refugee => pc.IsAlive(), + CustomRoles.NWitch => pc.IsAlive(), + CustomRoles.Wraith => pc.IsAlive(), + CustomRoles.Bomber => (Options.BomberCanKill.GetBool() && pc.IsAlive()), + CustomRoles.Nuker => (Options.BomberCanKill.GetBool() && pc.IsAlive()), + CustomRoles.Innocent => pc.IsAlive(), + CustomRoles.Counterfeiter => Counterfeiter.CanUseKillButton(pc.PlayerId), + CustomRoles.Pursuer => Pursuer.CanUseKillButton(pc.PlayerId), + CustomRoles.Morphling => Morphling.CanUseKillButton(pc.PlayerId), + CustomRoles.FFF => pc.IsAlive(), + CustomRoles.Medic => Medic.CanUseKillButton(pc.PlayerId), + CustomRoles.Gamer => pc.IsAlive(), + CustomRoles.DarkHide => DarkHide.CanUseKillButton(pc), + CustomRoles.Provocateur => pc.IsAlive(), + CustomRoles.Assassin => Assassin.CanUseKillButton(pc), + CustomRoles.BloodKnight => pc.IsAlive(), + CustomRoles.Crewpostor => false, + CustomRoles.Totocalcio => Totocalcio.CanUseKillButton(pc), + CustomRoles.Succubus => Succubus.CanUseKillButton(pc), + CustomRoles.CursedSoul => CursedSoul.CanUseKillButton(pc), + CustomRoles.Admirer => Admirer.CanUseKillButton(pc), + CustomRoles.Amnesiac => Amnesiac.CanUseKillButton(pc), + //CustomRoles.Warlock => !Main.isCurseAndKill.TryGetValue(pc.PlayerId, out bool wcs) || !wcs, + CustomRoles.Infectious => Infectious.CanUseKillButton(pc), + CustomRoles.Monarch => Monarch.CanUseKillButton(pc), + CustomRoles.Deputy => Deputy.CanUseKillButton(pc), + CustomRoles.Virus => pc.IsAlive(), + CustomRoles.Farseer => pc.IsAlive(), + CustomRoles.Spiritcaller => pc.IsAlive(), + CustomRoles.PlagueBearer => pc.IsAlive(), + CustomRoles.Pestilence => pc.IsAlive(), + CustomRoles.Pirate => pc.IsAlive(), + + _ => pc.Is(CustomRoleTypes.Impostor), + }; + } + public static bool CanUseImpostorVentButton(this PlayerControl pc) + { + if (!pc.IsAlive() || pc.Data.Role.Role == RoleTypes.GuardianAngel) return false; + if (CopyCat.playerIdList.Contains(pc.PlayerId)) return true; + + return pc.GetCustomRole() switch + { + CustomRoles.Minimalism or + CustomRoles.Sheriff or + CustomRoles.Deputy or + CustomRoles.Innocent or + // CustomRoles.SwordsMan or + CustomRoles.FFF or + CustomRoles.Medic or + // CustomRoles.NWitch or + CustomRoles.DarkHide or + CustomRoles.Monarch or + CustomRoles.Provocateur or + CustomRoles.Totocalcio or + CustomRoles.Succubus or + CustomRoles.CursedSoul or + CustomRoles.PlagueBearer or + CustomRoles.Admirer or + CustomRoles.Amnesiac or + CustomRoles.Glitch or + CustomRoles.Crusader or + CustomRoles.Wildling + => false, + + CustomRoles.Jackal => Jackal.CanVent.GetBool(), + CustomRoles.Sidekick => Jackal.CanVentSK.GetBool(), + CustomRoles.Poisoner => Poisoner.CanVent.GetBool(), + CustomRoles.NSerialKiller => NSerialKiller.CanVent.GetBool(), + CustomRoles.Pestilence => PlagueBearer.PestilenceCanVent.GetBool(), + CustomRoles.Medusa => Medusa.CanVent.GetBool(), + CustomRoles.Traitor => Traitor.CanVent.GetBool(), + CustomRoles.NWitch => NWitch.CanVent.GetBool(), + CustomRoles.Maverick => Maverick.CanVent.GetBool(), + CustomRoles.Jinx => Jinx.CanVent.GetBool(), + CustomRoles.Pelican => Pelican.CanVent.GetBool(), + CustomRoles.Gamer => Gamer.CanVent.GetBool(), + CustomRoles.BloodKnight => BloodKnight.CanVent.GetBool(), + CustomRoles.Juggernaut => Juggernaut.CanVent.GetBool(), + CustomRoles.Infectious => Infectious.CanVent.GetBool(), + CustomRoles.Ritualist => Ritualist.CanVent.GetBool(), + CustomRoles.Virus => Virus.CanVent.GetBool(), + CustomRoles.SwordsMan => SwordsMan.CanVent.GetBool(), + CustomRoles.Pickpocket => Pickpocket.CanVent.GetBool(), + CustomRoles.HexMaster => true, + CustomRoles.Wraith => true, + // CustomRoles.Chameleon => true, + CustomRoles.Parasite => true, + CustomRoles.Refugee => true, + CustomRoles.Spiritcaller => Spiritcaller.CanVent.GetBool(), + + CustomRoles.Arsonist => pc.IsDouseDone(), + CustomRoles.Revolutionist => pc.IsDrawDone(), + + //SoloKombat + CustomRoles.KB_Normal => true, + + _ => pc.Is(CustomRoleTypes.Impostor), + }; + } + public static bool IsDousedPlayer(this PlayerControl arsonist, PlayerControl target) + { + if (arsonist == null || target == null || Main.isDoused == null) return false; + Main.isDoused.TryGetValue((arsonist.PlayerId, target.PlayerId), out bool isDoused); + return isDoused; + } + public static bool IsDrawPlayer(this PlayerControl arsonist, PlayerControl target) + { + if (arsonist == null && target == null && Main.isDraw == null) return false; + Main.isDraw.TryGetValue((arsonist.PlayerId, target.PlayerId), out bool isDraw); + return isDraw; + } + public static bool IsRevealedPlayer(this PlayerControl player, PlayerControl target) + { + if (player == null || target == null || Main.isRevealed == null) return false; + Main.isRevealed.TryGetValue((player.PlayerId, target.PlayerId), out bool isDoused); + return isDoused; + } + public static void RpcSetDousedPlayer(this PlayerControl player, PlayerControl target, bool isDoused) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetDousedPlayer, SendOption.Reliable, -1);//RPCによる同期 + writer.Write(player.PlayerId); + writer.Write(target.PlayerId); + writer.Write(isDoused); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void RpcSetDrawPlayer(this PlayerControl player, PlayerControl target, bool isDoused) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetDrawPlayer, SendOption.Reliable, -1);//RPCによる同期 + writer.Write(player.PlayerId); + writer.Write(target.PlayerId); + writer.Write(isDoused); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void RpcSetRevealtPlayer(this PlayerControl player, PlayerControl target, bool isDoused) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRevealedPlayer, SendOption.Reliable, -1);//RPCによる同期 + writer.Write(player.PlayerId); + writer.Write(target.PlayerId); + writer.Write(isDoused); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ResetKillCooldown(this PlayerControl player) + { + Main.AllPlayerKillCooldown[player.PlayerId] = Options.DefaultKillCooldown; //キルクールをデフォルトキルクールに変更 + switch (player.GetCustomRole()) + { + case CustomRoles.SerialKiller: + SerialKiller.ApplyKillCooldown(player.PlayerId); //シリアルキラーはシリアルキラーのキルクールに。 + break; + case CustomRoles.TimeThief: + TimeThief.SetKillCooldown(player.PlayerId); //タイムシーフはタイムシーフのキルクールに。 + break; + /* case CustomRoles.Mare: + Mare.SetKillCooldown(player.PlayerId); + break; */ + case CustomRoles.EvilDiviner: + EvilDiviner.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Morphling: + Morphling.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Ritualist: + Ritualist.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Pickpocket: + Pickpocket.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Arsonist: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.ArsonistCooldown.GetFloat(); //アーソニストはアーソニストのキルクールに。 + break; + case CustomRoles.Inhibitor: + case CustomRoles.Saboteur: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.InhibitorCD.GetFloat(); //アーソニストはアーソニストのキルクールに。 + break; + case CustomRoles.Revolutionist: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.RevolutionistCooldown.GetFloat(); + break; + case CustomRoles.Underdog: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.UnderdogKillCooldown.GetFloat(); + break; + case CustomRoles.Jackal: + Jackal.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Sidekick: + Sidekick.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.PlagueBearer: + PlagueBearer.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Pestilence: + PlagueBearer.SetKillCooldownPestilence(player.PlayerId); + break; + + case CustomRoles.Councillor: + Councillor.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.HexMaster: + case CustomRoles.Wraith: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.DefaultKillCooldown; + break; + case CustomRoles.Parasite: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.ParasiteCD.GetFloat(); + break; + case CustomRoles.Refugee: + Main.AllPlayerKillCooldown[player.PlayerId] = Amnesiac.RefugeeKillCD.GetFloat(); + break; + case CustomRoles.NSerialKiller: + NSerialKiller.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Traitor: + Traitor.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Glitch: + Glitch.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.NWitch: + NWitch.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Maverick: + Maverick.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Jinx: + Jinx.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Poisoner: + Poisoner.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Sheriff: + Sheriff.SetKillCooldown(player.PlayerId); //シェリフはシェリフのキルクールに。 + break; + case CustomRoles.CopyCat: + CopyCat.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Minimalism: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.MNKillCooldown.GetFloat(); + break; + case CustomRoles.SwordsMan: + SwordsMan.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Zombie: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.ZombieKillCooldown.GetFloat(); + Main.AllPlayerSpeed[player.PlayerId] -= Options.ZombieSpeedReduce.GetFloat(); + break; + case CustomRoles.BoobyTrap: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.BTKillCooldown.GetFloat(); + break; + case CustomRoles.Scavenger: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.ScavengerKillCooldown.GetFloat(); + break; + case CustomRoles.Bomber: + case CustomRoles.Nuker: + if (Options.BomberCanKill.GetBool()) + Main.AllPlayerKillCooldown[player.PlayerId] = Options.BomberKillCD.GetFloat(); + else + Main.AllPlayerKillCooldown[player.PlayerId] = 300f; + break; + case CustomRoles.Capitalism: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.CapitalismSkillCooldown.GetFloat(); + break; + case CustomRoles.Pelican: + Main.AllPlayerKillCooldown[player.PlayerId] = Pelican.KillCooldown.GetFloat(); + break; + case CustomRoles.Counterfeiter: + Counterfeiter.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Pursuer: + Pursuer.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.FFF: + Main.AllPlayerKillCooldown[player.PlayerId] = 0f; + break; + case CustomRoles.Medusa: + Medusa.SetKillCooldown(player.PlayerId); + break; + + case CustomRoles.Cleaner: + Main.AllPlayerKillCooldown[player.PlayerId] = Options.CleanerKillCooldown.GetFloat(); + break; + case CustomRoles.Medic: + Medic.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Gamer: + Gamer.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.BallLightning: + BallLightning.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.DarkHide: + DarkHide.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Greedier: + Greedier.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.QuickShooter: + QuickShooter.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Provocateur: + Main.AllPlayerKillCooldown[player.PlayerId] = 0f; + break; + case CustomRoles.Assassin: + Assassin.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Sans: + Sans.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Juggernaut: + Juggernaut.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Reverie: + Reverie.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Hacker: + Hacker.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.KB_Normal: + Main.AllPlayerKillCooldown[player.PlayerId] = SoloKombatManager.KB_ATKCooldown.GetFloat(); + break; + case CustomRoles.BloodKnight: + BloodKnight.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Totocalcio: + Totocalcio.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Gangster: + Gangster.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Succubus: + Succubus.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.CursedSoul: + CursedSoul.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Admirer: + Admirer.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Amnesiac: + Amnesiac.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Infectious: + Infectious.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Monarch: + Monarch.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Deputy: + Deputy.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Virus: + Virus.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Farseer: + Farseer.SetCooldown(player.PlayerId); + break; + case CustomRoles.Dazzler: + Dazzler.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Deathpact: + Deathpact.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Devourer: + Devourer.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Spiritcaller: + Spiritcaller.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Lurker: + Lurker.SetKillCooldown(player.PlayerId); + break; + case CustomRoles.Crusader: + Crusader.SetKillCooldown(player.PlayerId); + break; + } + if (player.PlayerId == LastImpostor.currentId) + LastImpostor.SetKillCooldown(); + if (player.Is(CustomRoles.Mare)) + Main.AllPlayerKillCooldown[player.PlayerId] = Options.MareKillCD.GetFloat(); + + if (Main.KilledDiseased.ContainsKey(player.PlayerId)) + { + Main.AllPlayerKillCooldown[player.PlayerId] = Main.AllPlayerKillCooldown[player.PlayerId] + Main.KilledDiseased[player.PlayerId] * Options.DiseasedCDOpt.GetFloat(); + Logger.Info($"kill cd of player set to {Main.AllPlayerKillCooldown[player.PlayerId]}", "Diseased"); + } + if (Main.KilledAntidote.ContainsKey(player.PlayerId)) + { + var kcd = Main.AllPlayerKillCooldown[player.PlayerId] - Main.KilledAntidote[player.PlayerId] * Options.AntidoteCDOpt.GetFloat(); + if (kcd < 0) kcd = 0; + Main.AllPlayerKillCooldown[player.PlayerId] = kcd; + Logger.Info($"kill cd of player set to {Main.AllPlayerKillCooldown[player.PlayerId]}", "Antidote"); + } + } + public static void TrapperKilled(this PlayerControl killer, PlayerControl target) + { + Logger.Info($"{target?.Data?.PlayerName}はTrapperだった", "Trapper"); + var tmpSpeed = Main.AllPlayerSpeed[killer.PlayerId]; + Main.AllPlayerSpeed[killer.PlayerId] = Main.MinSpeed; //tmpSpeedで後ほど値を戻すので代入しています。 + ReportDeadBodyPatch.CanReport[killer.PlayerId] = false; + killer.MarkDirtySettings(); + new LateTask(() => + { + Main.AllPlayerSpeed[killer.PlayerId] = Main.AllPlayerSpeed[killer.PlayerId] - Main.MinSpeed + tmpSpeed; + ReportDeadBodyPatch.CanReport[killer.PlayerId] = true; + killer.MarkDirtySettings(); + RPC.PlaySoundRPC(killer.PlayerId, Sounds.TaskComplete); + }, Options.TrapperBlockMoveTime.GetFloat(), "Trapper BlockMove"); + } + public static bool IsDouseDone(this PlayerControl player) + { + if (!player.Is(CustomRoles.Arsonist)) return false; + var count = Utils.GetDousedPlayerCount(player.PlayerId); + return count.Item1 >= count.Item2; + } + public static bool IsDrawDone(this PlayerControl player)//判断是否拉拢完成 + { + if (!player.Is(CustomRoles.Revolutionist)) return false; + var count = Utils.GetDrawPlayerCount(player.PlayerId, out var _); + return count.Item1 >= count.Item2; + } + public static void RpcExileV2(this PlayerControl player) + { + player.Exiled(); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.Exiled, SendOption.None, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void RpcMurderPlayerV3(this PlayerControl killer, PlayerControl target) + { + //用于TOHE的击杀前判断 + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) return; + + if (killer.PlayerId == target.PlayerId && killer.shapeshifting) + { + new LateTask(() => { killer.RpcMurderPlayer(target); }, 1.5f, "Shapeshifting Suicide Delay"); + return; + } + + killer.RpcMurderPlayer(target); + } + public static void RpcMurderPlayerV2(this PlayerControl killer, PlayerControl target) + { + if (target == null) target = killer; + if (AmongUsClient.Instance.AmClient) + { + killer.MurderPlayer(target); + } + MessageWriter messageWriter = AmongUsClient.Instance.StartRpcImmediately(killer.NetId, (byte)RpcCalls.MurderPlayer, SendOption.None, -1); + messageWriter.WriteNetObject(target); + AmongUsClient.Instance.FinishRpcImmediately(messageWriter); + Utils.NotifyRoles(); + } + public static bool RpcCheckAndMurder(this PlayerControl killer, PlayerControl target, bool check = false) => CheckMurderPatch.RpcCheckAndMurder(killer, target, check); + public static void NoCheckStartMeeting(this PlayerControl reporter, GameData.PlayerInfo target, bool force = false) + { /*サボタージュ中でも関係なしに会議を起こせるメソッド + targetがnullの場合はボタンとなる*/ + if (Options.DisableMeeting.GetBool() && !force) return; + ReportDeadBodyPatch.AfterReportTasks(reporter, target); + MeetingRoomManager.Instance.AssignSelf(reporter, target); + DestroyableSingleton.Instance.OpenMeetingRoom(reporter); + reporter.RpcStartMeeting(target); + } + public static bool IsModClient(this PlayerControl player) => Main.playerVersion.ContainsKey(player.PlayerId); + /// + ///プレイヤーのRoleBehaviourのGetPlayersInAbilityRangeSortedを実行し、戻り値を返します。 + /// + ///trueにすると、壁の向こう側のプレイヤーが含まれるようになります。守護天使用 + ///GetPlayersInAbilityRangeSortedの戻り値 + public static List GetPlayersInAbilityRangeSorted(this PlayerControl player, bool ignoreColliders = false) => GetPlayersInAbilityRangeSorted(player, pc => true, ignoreColliders); + /// + ///プレイヤーのRoleBehaviourのGetPlayersInAbilityRangeSortedを実行し、predicateの条件に合わないものを除外して返します。 + /// + ///リストに入れるプレイヤーの条件 このpredicateに入れてfalseを返すプレイヤーは除外されます。 + ///trueにすると、壁の向こう側のプレイヤーが含まれるようになります。守護天使用 + ///GetPlayersInAbilityRangeSortedの戻り値から条件に合わないプレイヤーを除外したもの。 + public static List GetPlayersInAbilityRangeSorted(this PlayerControl player, Predicate predicate, bool ignoreColliders = false) + { + var rangePlayersIL = RoleBehaviour.GetTempPlayerList(); + List rangePlayers = new(); + player.Data.Role.GetPlayersInAbilityRangeSorted(rangePlayersIL, ignoreColliders); + foreach (var pc in rangePlayersIL) + { + if (predicate(pc)) rangePlayers.Add(pc); + } + return rangePlayers; + } + public static bool IsNeutralKiller(this PlayerControl player) => player.GetCustomRole().IsNK(); + public static bool IsNeutralBenign(this PlayerControl player) => player.GetCustomRole().IsNB(); + public static bool IsNeutralEvil(this PlayerControl player) => player.GetCustomRole().IsNC(); + public static bool IsNeutralChaos(this PlayerControl player) => player.GetCustomRole().IsNC(); + public static bool IsNonNeutralKiller(this PlayerControl player) => player.GetCustomRole().IsNonNK(); + public static bool IsSnitchTarget(this PlayerControl player) => player.GetCustomRole().IsSnitchTarget(); + public static bool KnowDeathReason(this PlayerControl seer, PlayerControl target) + => (seer.Is(CustomRoles.Doctor) || seer.Is(CustomRoles.Autopsy) + || (seer.Data.IsDead && Options.GhostCanSeeDeathReason.GetBool())) + && target.Data.IsDead || target.Is(CustomRoles.Gravestone) && target.Data.IsDead; + public static bool KnowDeadTeam(this PlayerControl seer, PlayerControl target) + => (seer.Is(CustomRoles.Necroview)) + && target.Data.IsDead; + + public static bool KnowLivingTeam(this PlayerControl seer, PlayerControl target) + => (seer.Is(CustomRoles.Visionary)) + && !target.Data.IsDead; + public static string GetRoleInfo(this PlayerControl player, bool InfoLong = false) + { + var role = player.GetCustomRole(); + if (role is CustomRoles.Crewmate or CustomRoles.Impostor) + InfoLong = false; + + var text = role.ToString(); + + var Prefix = ""; + if (!InfoLong) + switch (role) + { + case CustomRoles.Mafia: + Prefix = Utils.CanMafiaKill() ? "After" : "Before"; + break; + }; + var Info = (role.IsVanilla() ? "Blurb" : "Info") + (InfoLong ? "Long" : ""); + return GetString($"{Prefix}{text}{Info}"); + } + public static void SetRealKiller(this PlayerControl target, PlayerControl killer, bool NotOverRide = false) + { + if (target == null) + { + Logger.Info("target=null", "SetRealKiller"); + return; + } + var State = Main.PlayerStates[target.PlayerId]; + if (State.RealKiller.Item1 != DateTime.MinValue && NotOverRide) return; //既に値がある場合上書きしない + byte killerId = killer == null ? byte.MaxValue : killer.PlayerId; + RPC.SetRealKiller(target.PlayerId, killerId); + } + public static PlayerControl GetRealKiller(this PlayerControl target) + { + var killerId = Main.PlayerStates[target.PlayerId].GetRealKiller(); + return killerId == byte.MaxValue ? null : Utils.GetPlayerById(killerId); + } + public static PlainShipRoom GetPlainShipRoom(this PlayerControl pc) + { + if (!pc.IsAlive() || Pelican.IsEaten(pc.PlayerId)) return null; + var Rooms = ShipStatus.Instance.AllRooms; + if (Rooms == null) return null; + foreach (var room in Rooms) + { + if (!room.roomArea) continue; + if (pc.Collider.IsTouching(room.roomArea)) + return room; + } + return null; + } + + //汎用 + public static bool Is(this PlayerControl target, CustomRoles role) => + role > CustomRoles.NotAssigned ? target.GetCustomSubRoles().Contains(role) : target.GetCustomRole() == role; + public static bool Is(this PlayerControl target, CustomRoleTypes type) { return target.GetCustomRole().GetCustomRoleTypes() == type; } + public static bool Is(this PlayerControl target, RoleTypes type) { return target.GetCustomRole().GetRoleTypes() == type; } + public static bool Is(this PlayerControl target, CountTypes type) { return target.GetCountTypes() == type; } + public static bool IsAlive(this PlayerControl target) + { + //ロビーなら生きている + //targetがnullならば切断者なので生きていない + //targetがnullでなく取得できない場合は登録前なので生きているとする + if (target == null || target.Is(CustomRoles.GM)) return false; + return GameStates.IsLobby || (target != null && (!Main.PlayerStates.TryGetValue(target.PlayerId, out var ps) || !ps.IsDead)); + } + public static bool IsExiled(this PlayerControl target) + { + return GameStates.InGame || (target != null && (Main.PlayerStates[target.PlayerId].deathReason == PlayerState.DeathReason.Vote)); + } + +} \ No newline at end of file diff --git a/Modules/Extensions/IGameManagerEx.cs b/Modules/Extensions/IGameManagerEx.cs new file mode 100644 index 000000000..74d0c6e4d --- /dev/null +++ b/Modules/Extensions/IGameManagerEx.cs @@ -0,0 +1,26 @@ +using AmongUs.GameOptions; + +namespace TOHE.Modules.Extensions; + +public static class IGameManagerEx +{ + public static void Set(this BoolOptionNames name, bool value, IGameOptions opt) => opt.SetBool(name, value); + public static void Set(this BoolOptionNames name, bool value, NormalGameOptionsV07 opt) => opt.SetBool(name, value); + public static void Set(this BoolOptionNames name, bool value, HideNSeekGameOptionsV07 opt) => opt.SetBool(name, value); + + public static void Set(this Int32OptionNames name, int value, IGameOptions opt) => opt.SetInt(name, value); + public static void Set(this Int32OptionNames name, int value, NormalGameOptionsV07 opt) => opt.SetInt(name, value); + public static void Set(this Int32OptionNames name, int value, HideNSeekGameOptionsV07 opt) => opt.SetInt(name, value); + + public static void Set(this FloatOptionNames name, float value, IGameOptions opt) => opt.SetFloat(name, value); + public static void Set(this FloatOptionNames name, float value, NormalGameOptionsV07 opt) => opt.SetFloat(name, value); + public static void Set(this FloatOptionNames name, float value, HideNSeekGameOptionsV07 opt) => opt.SetFloat(name, value); + + public static void Set(this ByteOptionNames name, byte value, IGameOptions opt) => opt.SetByte(name, value); + public static void Set(this ByteOptionNames name, byte value, NormalGameOptionsV07 opt) => opt.SetByte(name, value); + public static void Set(this ByteOptionNames name, byte value, HideNSeekGameOptionsV07 opt) => opt.SetByte(name, value); + + public static void Set(this UInt32OptionNames name, uint value, IGameOptions opt) => opt.SetUInt(name, value); + public static void Set(this UInt32OptionNames name, uint value, NormalGameOptionsV07 opt) => opt.SetUInt(name, value); + public static void Set(this UInt32OptionNames name, uint value, HideNSeekGameOptionsV07 opt) => opt.SetUInt(name, value); +} \ No newline at end of file diff --git a/Modules/GameOptionsSender/GameOptionsSender.cs b/Modules/GameOptionsSender/GameOptionsSender.cs new file mode 100644 index 000000000..8cb485344 --- /dev/null +++ b/Modules/GameOptionsSender/GameOptionsSender.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using AmongUs.GameOptions; +using Hazel; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem; +using InnerNet; + +namespace TOHE.Modules; + +public abstract class GameOptionsSender +{ + #region Static + public readonly static List AllSenders = new(15) { new NormalGameOptionsSender() }; + + public static void SendAllGameOptions() + { + AllSenders.RemoveAll(s => !s.AmValid()); + foreach (var sender in AllSenders) + { + if (sender.IsDirty) sender.SendGameOptions(); + sender.IsDirty = false; + } + } + #endregion + + public abstract IGameOptions BasedGameOptions { get; } + public abstract bool IsDirty { get; protected set; } + + + public virtual void SendGameOptions() + { + var opt = BuildGameOptions(); + + // option => byte[] + MessageWriter writer = MessageWriter.Get(SendOption.None); + writer.Write(opt.Version); + writer.StartMessage(0); + writer.Write((byte)opt.GameMode); + if (opt.TryCast(out var normalOpt)) + NormalGameOptionsV07.Serialize(writer, normalOpt); + else if (opt.TryCast(out var hnsOpt)) + HideNSeekGameOptionsV07.Serialize(writer, hnsOpt); + else + { + writer.Recycle(); + Logger.Error("オプションのキャストに失敗しました", this.ToString()); + } + writer.EndMessage(); + + // 配列化&送信 + var byteArray = new Il2CppStructArray(writer.Length - 1); + // MessageWriter.ToByteArray + Buffer.BlockCopy(writer.Buffer.Cast(), 1, byteArray.Cast(), 0, writer.Length - 1); + + SendOptionsArray(byteArray); + writer.Recycle(); + } + public virtual void SendOptionsArray(Il2CppStructArray optionArray) + { + for (byte i = 0; i < GameManager.Instance.LogicComponents.Count; i++) + { + if (GameManager.Instance.LogicComponents[i].TryCast(out _)) + { + SendOptionsArray(optionArray, i, -1); + } + } + } + protected virtual void SendOptionsArray(Il2CppStructArray optionArray, byte LogicOptionsIndex, int targetClientId) + { + var writer = MessageWriter.Get(SendOption.Reliable); + + writer.StartMessage(targetClientId == -1 ? Tags.GameData : Tags.GameDataTo); + { + writer.Write(AmongUsClient.Instance.GameId); + if (targetClientId != -1) writer.WritePacked(targetClientId); + writer.StartMessage(1); + { + writer.WritePacked(GameManager.Instance.NetId); + writer.StartMessage(LogicOptionsIndex); + { + writer.WriteBytesAndSize(optionArray); + } + writer.EndMessage(); + } + writer.EndMessage(); + } + writer.EndMessage(); + + AmongUsClient.Instance.SendOrDisconnect(writer); + writer.Recycle(); + } + public abstract IGameOptions BuildGameOptions(); + + public virtual bool AmValid() => true; +} \ No newline at end of file diff --git a/Modules/GameOptionsSender/NormalGameOptionsSender.cs b/Modules/GameOptionsSender/NormalGameOptionsSender.cs new file mode 100644 index 000000000..28f2c08d4 --- /dev/null +++ b/Modules/GameOptionsSender/NormalGameOptionsSender.cs @@ -0,0 +1,31 @@ +using AmongUs.GameOptions; + +namespace TOHE.Modules; + +public class NormalGameOptionsSender : GameOptionsSender +{ + public override IGameOptions BasedGameOptions => + GameOptionsManager.Instance.CurrentGameOptions; + public override bool IsDirty + { + get + { + if (_logicOptions == null || !GameManager.Instance.LogicComponents.Contains(_logicOptions)) + { + foreach (var glc in GameManager.Instance.LogicComponents) + if (glc.TryCast(out var lo)) + _logicOptions = lo; + } + return _logicOptions != null && _logicOptions.IsDirty; + } + protected set + { + if (_logicOptions != null) + _logicOptions.ClearDirtyFlag(); + } + } + private LogicOptions _logicOptions; + + public override IGameOptions BuildGameOptions() + => BasedGameOptions; +} \ No newline at end of file diff --git a/Modules/GameOptionsSender/PlayerGameOptionsSender.cs b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs new file mode 100644 index 000000000..9d79df9c6 --- /dev/null +++ b/Modules/GameOptionsSender/PlayerGameOptionsSender.cs @@ -0,0 +1,501 @@ +using System.Linq; +using AmongUs.GameOptions; +using Hazel; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppSystem.Linq; +using InnerNet; +using Mathf = UnityEngine.Mathf; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Impostor; +using TOHE.Roles.Neutral; + +namespace TOHE.Modules; + +public class PlayerGameOptionsSender : GameOptionsSender +{ + public static void SetDirty(PlayerControl player) => SetDirty(player.PlayerId); + public static void SetDirty(byte playerId) => + AllSenders.OfType() + .Where(sender => sender.player.PlayerId == playerId) + .ToList().ForEach(sender => sender.SetDirty()); + public static void SetDirtyToAll() => + AllSenders.OfType() + .ToList().ForEach(sender => sender.SetDirty()); + + public override IGameOptions BasedGameOptions => + Main.RealOptionsData.Restore(new NormalGameOptionsV07(new UnityLogger().Cast()).Cast()); + public override bool IsDirty { get; protected set; } + + public PlayerControl player; + + public PlayerGameOptionsSender(PlayerControl player) + { + this.player = player; + } + public void SetDirty() => IsDirty = true; + + public override void SendGameOptions() + { + if (player.AmOwner) + { + var opt = BuildGameOptions(); + foreach (var com in GameManager.Instance.LogicComponents) + { + if (com.TryCast(out var lo)) + lo.SetGameOptions(opt); + } + GameOptionsManager.Instance.CurrentGameOptions = opt; + } + else base.SendGameOptions(); + } + + public override void SendOptionsArray(Il2CppStructArray optionArray) + { + for (byte i = 0; i < GameManager.Instance.LogicComponents.Count; i++) + { + if (GameManager.Instance.LogicComponents[i].TryCast(out _)) + { + SendOptionsArray(optionArray, i, player.GetClientId()); + } + } + } + public static void RemoveSender(PlayerControl player) + { + var sender = AllSenders.OfType() + .FirstOrDefault(sender => sender.player.PlayerId == player.PlayerId); + if (sender == null) return; + sender.player = null; + AllSenders.Remove(sender); + } + public override IGameOptions BuildGameOptions() + { + if (Main.RealOptionsData == null) + { + Main.RealOptionsData = new OptionBackupData(GameOptionsManager.Instance.CurrentGameOptions); + } + + var opt = BasedGameOptions; + AURoleOptions.SetOpt(opt); + var state = Main.PlayerStates[player.PlayerId]; + opt.BlackOut(state.IsBlackOut); + + CustomRoles role = player.GetCustomRole(); + switch (role.GetCustomRoleTypes()) + { + case CustomRoleTypes.Impostor: + AURoleOptions.ShapeshifterCooldown = Options.DefaultShapeshiftCooldown.GetFloat(); + AURoleOptions.GuardianAngelCooldown = Spiritcaller.SpiritAbilityCooldown.GetFloat(); + break; + case CustomRoleTypes.Neutral: + AURoleOptions.GuardianAngelCooldown = Spiritcaller.SpiritAbilityCooldown.GetFloat(); + break; + case CustomRoleTypes.Crewmate: + AURoleOptions.GuardianAngelCooldown = Spiritcaller.SpiritAbilityCooldown.GetFloat(); + break; + } + + switch (role) + { + case CustomRoles.Terrorist: + case CustomRoles.SabotageMaster: + // case CustomRoles.Mario: + case CustomRoles.EngineerTOHE: + case CustomRoles.Phantom: + case CustomRoles.Crewpostor: + // case CustomRoles.Jester: + case CustomRoles.Chameleon: + AURoleOptions.EngineerCooldown = 0f; + AURoleOptions.EngineerInVentMaxTime = 0f; + break; + case CustomRoles.ShapeMaster: + AURoleOptions.ShapeshifterCooldown = 1f; + AURoleOptions.ShapeshifterLeaveSkin = false; + AURoleOptions.ShapeshifterDuration = Options.ShapeMasterShapeshiftDuration.GetFloat(); + break; + case CustomRoles.Warlock: + AURoleOptions.ShapeshifterCooldown = Main.isCursed ? 1f : Options.DefaultKillCooldown; + AURoleOptions.ShapeshifterDuration = Options.WarlockShiftDuration.GetFloat(); + break; + case CustomRoles.SerialKiller: + SerialKiller.ApplyGameOptions(player); + break; + case CustomRoles.Tracefinder: + Tracefinder.ApplyGameOptions(); + break; + case CustomRoles.BountyHunter: + BountyHunter.ApplyGameOptions(); + break; + case CustomRoles.Sheriff: + case CustomRoles.SwordsMan: + case CustomRoles.Arsonist: + // case CustomRoles.Minimalism: + case CustomRoles.Innocent: + case CustomRoles.Pelican: + case CustomRoles.Revolutionist: + case CustomRoles.Medic: + case CustomRoles.Crusader: + case CustomRoles.Provocateur: + case CustomRoles.Monarch: + case CustomRoles.Deputy: + case CustomRoles.Counterfeiter: + case CustomRoles.Succubus: + case CustomRoles.CursedSoul: + case CustomRoles.Admirer: + case CustomRoles.Amnesiac: + opt.SetVision(false); + break; + case CustomRoles.Pestilence: + opt.SetVision(PlagueBearer.PestilenceHasImpostorVision.GetBool()); + break; + case CustomRoles.Refugee: + opt.SetVision(true); + break; + case CustomRoles.Virus: + opt.SetVision(Virus.ImpostorVision.GetBool()); + break; + case CustomRoles.Zombie: + opt.SetFloat(FloatOptionNames.ImpostorLightMod, 0.2f); + break; + case CustomRoles.Doctor: + AURoleOptions.ScientistCooldown = 0f; + AURoleOptions.ScientistBatteryCharge = Options.DoctorTaskCompletedBatteryCharge.GetFloat(); + break; + case CustomRoles.Mayor: + AURoleOptions.EngineerCooldown = + !Main.MayorUsedButtonCount.TryGetValue(player.PlayerId, out var count) || count < Options.MayorNumOfUseButton.GetInt() + ? opt.GetInt(Int32OptionNames.EmergencyCooldown) + : 300f; + AURoleOptions.EngineerInVentMaxTime = 1; + break; + case CustomRoles.Paranoia: + AURoleOptions.EngineerCooldown = + !Main.ParaUsedButtonCount.TryGetValue(player.PlayerId, out var count2) || count2 < Options.ParanoiaNumOfUseButton.GetInt() + ? Options.ParanoiaVentCooldown.GetFloat() + : 300f; + AURoleOptions.EngineerInVentMaxTime = 1; + break; + /* case CustomRoles.Mare: + Mare.ApplyGameOptions(player.PlayerId); + break; */ + case CustomRoles.EvilTracker: + EvilTracker.ApplyGameOptions(player.PlayerId); + break; + case CustomRoles.ShapeshifterTOHE: + AURoleOptions.ShapeshifterCooldown = Options.ShapeshiftCD.GetFloat(); + AURoleOptions.ShapeshifterDuration = Options.ShapeshiftDur.GetFloat(); + break; + case CustomRoles.Bomber: + AURoleOptions.ShapeshifterCooldown = Options.BombCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = 2f; + break; + case CustomRoles.Nuker: + AURoleOptions.ShapeshifterCooldown = Options.NukeCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = 2f; + break; + case CustomRoles.Mafia: + AURoleOptions.ShapeshifterCooldown = Options.MafiaShapeshiftCD.GetFloat(); + AURoleOptions.ShapeshifterDuration = Options.MafiaShapeshiftDur.GetFloat(); + break; + case CustomRoles.ScientistTOHE: + AURoleOptions.ScientistCooldown = Options.ScientistCD.GetFloat(); + AURoleOptions.ScientistBatteryCharge = Options.ScientistDur.GetFloat(); + break; + case CustomRoles.Wildling: + AURoleOptions.ShapeshifterCooldown = Wildling.ShapeshiftCD.GetFloat(); + AURoleOptions.ShapeshifterDuration = Wildling.ShapeshiftDur.GetFloat(); + break; + case CustomRoles.Jackal: + Jackal.ApplyGameOptions(opt); + break; + case CustomRoles.Sidekick: + Sidekick.ApplyGameOptions(opt); + break; + case CustomRoles.Vulture: + Vulture.ApplyGameOptions(opt); + AURoleOptions.EngineerCooldown = 0f; + AURoleOptions.EngineerInVentMaxTime = 0f; + break; + case CustomRoles.Poisoner: + Poisoner.ApplyGameOptions(opt); + break; + case CustomRoles.Veteran: + AURoleOptions.EngineerCooldown = + !Main.VeteranNumOfUsed.TryGetValue(player.PlayerId, out var count3) || count3 > 0 + ? Options.VeteranSkillCooldown.GetFloat() + : 300f; + AURoleOptions.EngineerInVentMaxTime = 1; + break; + case CustomRoles.Grenadier: + AURoleOptions.EngineerCooldown = Options.GrenadierSkillCooldown.GetFloat(); + AURoleOptions.EngineerInVentMaxTime = 1; + break; + /* case CustomRoles.Flashbang: + AURoleOptions.ShapeshifterCooldown = Options.FlashbangSkillCooldown.GetFloat(); + AURoleOptions.ShapeshifterDuration = Options.FlashbangSkillDuration.GetFloat(); + break; */ + case CustomRoles.TimeMaster: + AURoleOptions.EngineerCooldown = Options.TimeMasterSkillCooldown.GetFloat(); + AURoleOptions.EngineerInVentMaxTime = 1; + break; + case CustomRoles.FFF: + case CustomRoles.Pursuer: + opt.SetVision(true); + break; + case CustomRoles.NSerialKiller: + NSerialKiller.ApplyGameOptions(opt); + break; + case CustomRoles.Morphling: + Morphling.ApplyGameOptions(); + break; + case CustomRoles.Traitor: + Traitor.ApplyGameOptions(opt); + break; + case CustomRoles.Glitch: + Glitch.ApplyGameOptions(opt); + break; + case CustomRoles.NWitch: + NWitch.ApplyGameOptions(opt); + break; + case CustomRoles.Maverick: + Maverick.ApplyGameOptions(opt); + break; + case CustomRoles.Medusa: + Medusa.ApplyGameOptions(opt); + break; + case CustomRoles.Jinx: + Jinx.ApplyGameOptions(opt); + break; + case CustomRoles.Ritualist: + Ritualist.ApplyGameOptions(opt); + break; + case CustomRoles.Pickpocket: + Pickpocket.ApplyGameOptions(opt); + break; + case CustomRoles.Juggernaut: + opt.SetVision(Juggernaut.HasImpostorVision.GetBool()); + break; + case CustomRoles.Reverie: + opt.SetVision(false); + break; + case CustomRoles.Jester: + AURoleOptions.EngineerCooldown = 0f; + AURoleOptions.EngineerInVentMaxTime = 0f; + opt.SetVision(Options.JesterHasImpostorVision.GetBool()); + break; + case CustomRoles.Infectious: + opt.SetVision(Infectious.HasImpostorVision.GetBool()); + break; + case CustomRoles.Lawyer: + //Main.NormalOptions.CrewLightMod = Lawyer.LawyerVision.GetFloat(); + break; + case CustomRoles.Wraith: + case CustomRoles.HexMaster: + case CustomRoles.Parasite: + opt.SetVision(true); + break; + /* case CustomRoles.Chameleon: + opt.SetVision(false); + break; */ + + case CustomRoles.Gamer: + Gamer.ApplyGameOptions(opt); + break; + case CustomRoles.DarkHide: + DarkHide.ApplyGameOptions(opt); + break; + case CustomRoles.Workaholic: + AURoleOptions.EngineerCooldown = Options.WorkaholicVentCooldown.GetFloat(); + AURoleOptions.EngineerInVentMaxTime = 0f; + break; + case CustomRoles.ImperiusCurse: + AURoleOptions.ShapeshifterCooldown = Options.ImperiusCurseShapeshiftCooldown.GetFloat(); + AURoleOptions.ShapeshifterLeaveSkin = false; + AURoleOptions.ShapeshifterDuration = Options.ShapeImperiusCurseShapeshiftDuration.GetFloat(); + break; + case CustomRoles.QuickShooter: + AURoleOptions.ShapeshifterCooldown = QuickShooter.ShapeshiftCooldown.GetFloat(); + break; + case CustomRoles.Camouflager: + Camouflager.ApplyGameOptions(); + break; + case CustomRoles.Assassin: + Assassin.ApplyGameOptions(); + break; + case CustomRoles.Hacker: + Hacker.ApplyGameOptions(); + break; + case CustomRoles.Hangman: + Hangman.ApplyGameOptions(); + break; + case CustomRoles.Sunnyboy: + AURoleOptions.ScientistCooldown = 0f; + AURoleOptions.ScientistBatteryCharge = 60f; + break; + case CustomRoles.BloodKnight: + BloodKnight.ApplyGameOptions(opt); + break; + case CustomRoles.DovesOfNeace: + AURoleOptions.EngineerCooldown = + !Main.DovesOfNeaceNumOfUsed.TryGetValue(player.PlayerId, out var count4) || count4 > 0 + ? Options.DovesOfNeaceCooldown.GetFloat() + : 300f; + AURoleOptions.EngineerInVentMaxTime = 1; + break; + case CustomRoles.Disperser: + Disperser.ApplyGameOptions(); + break; + case CustomRoles.Farseer: + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, Farseer.Vision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Farseer.Vision.GetFloat()); + break; + case CustomRoles.Dazzler: + Dazzler.ApplyGameOptions(); + break; + case CustomRoles.Devourer: + Devourer.ApplyGameOptions(); + break; + case CustomRoles.Addict: + AURoleOptions.EngineerCooldown = Addict.VentCooldown.GetFloat(); + AURoleOptions.EngineerInVentMaxTime = 1; + break; + case CustomRoles.Mario: + AURoleOptions.EngineerCooldown = Options.MarioVentCD.GetFloat(); + AURoleOptions.EngineerInVentMaxTime = 1; + break; + case CustomRoles.Deathpact: + Deathpact.ApplyGameOptions(); + break; + case CustomRoles.Twister: + Twister.ApplyGameOptions(); + break; + case CustomRoles.Spiritcaller: + opt.SetVision(Spiritcaller.ImpostorVision.GetBool()); + break; + } + + // Ϊ�Ի��ߵ����� + if (Main.AllPlayerControls.Where(x => x.Is(CustomRoles.Bewilder) && !x.IsAlive() && x.GetRealKiller()?.PlayerId == player.PlayerId && !x.Is(CustomRoles.Hangman)).Count() > 0) + { + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.BewilderVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.BewilderVision.GetFloat()); + } + if (Main.AllPlayerControls.Where(x => x.Is(CustomRoles.Ghoul) && !x.IsAlive() && x.GetRealKiller()?.PlayerId == player.PlayerId).Count() > 0) + { + Main.KillGhoul.Add(player.PlayerId); + } + /* if (Main.AllPlayerControls.Where(x => x.Is(CustomRoles.Diseased) && !x.IsAlive() && x.GetRealKiller()?.PlayerId == player.PlayerId).Count() > 0) + { + Main.AllPlayerKillCooldown[player.PlayerId] *= Options.DiseasedMultiplier.GetFloat(); + player.SetKillCooldownV3(); + player.ResetKillCooldown(); + // player.SyncSettings(); + } */ + + // Ͷ��ɵ�ϵ������������� + if ( + (Main.GrenadierBlinding.Count >= 1 && + (player.GetCustomRole().IsImpostor() || + (player.GetCustomRole().IsNeutral() && Options.GrenadierCanAffectNeutral.GetBool())) + ) || ( + Main.MadGrenadierBlinding.Count >= 1 && !player.GetCustomRole().IsImpostorTeam() && !player.Is(CustomRoles.Madmate)) + ) + { + { + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.GrenadierCauseVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.GrenadierCauseVision.GetFloat()); + } + } + /* if ((Main.FlashbangInProtect.Count >= 1 && Main.ForFlashbang.Contains(player.PlayerId) && (!player.GetCustomRole().IsCrewmate()))) + { + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.FlashbangVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.FlashbangVision.GetFloat()); + } */ + + Dazzler.SetDazzled(player, opt); + Deathpact.SetDeathpactVision(player, opt); + + Spiritcaller.ReduceVision(opt, player); + + foreach (var subRole in Main.PlayerStates[player.PlayerId].SubRoles) + { + switch (subRole) + { + case CustomRoles.Watcher: + opt.SetBool(BoolOptionNames.AnonymousVotes, false); + break; + case CustomRoles.Flashman: + Main.AllPlayerSpeed[player.PlayerId] = Options.FlashmanSpeed.GetFloat(); + break; + case CustomRoles.Lighter: + if (!Utils.IsActive(SystemTypes.Electrical)) + opt.SetVision(true); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.LighterVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.LighterVision.GetFloat()); + if (Utils.IsActive(SystemTypes.Electrical)) + opt.SetVision(true); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.LighterVision.GetFloat() * 5); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.LighterVision.GetFloat() * 5); + break; + case CustomRoles.Bewilder: + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.BewilderVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.BewilderVision.GetFloat()); + break; + case CustomRoles.Sunglasses: + opt.SetVision(false); + opt.SetFloat(FloatOptionNames.CrewLightMod, Options.SunglassesVision.GetFloat()); + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Options.SunglassesVision.GetFloat()); + break; + case CustomRoles.Reach: + opt.SetInt(Int32OptionNames.KillDistance, 2); + break; + case CustomRoles.Madmate: + opt.SetVision(Options.MadmateHasImpostorVision.GetBool()); + break; + } + } + + // ������������ȴΪ0ʱ�޷�������ʾͼ�� + AURoleOptions.EngineerCooldown = Mathf.Max(0.01f, AURoleOptions.EngineerCooldown); + + if (Main.AllPlayerKillCooldown.TryGetValue(player.PlayerId, out var killCooldown)) + { + AURoleOptions.KillCooldown = Mathf.Max(0.01f, killCooldown); + } + + if (Main.AllPlayerSpeed.TryGetValue(player.PlayerId, out var speed)) + { + AURoleOptions.PlayerSpeedMod = Mathf.Clamp(speed, Main.MinSpeed, 3f); + } + + state.taskState.hasTasks = Utils.HasTasks(player.Data, false); + if (Options.GhostCanSeeOtherVotes.GetBool() && player.Data.IsDead) + opt.SetBool(BoolOptionNames.AnonymousVotes, false); + if (Options.AdditionalEmergencyCooldown.GetBool() && + Options.AdditionalEmergencyCooldownThreshold.GetInt() <= Utils.AllAlivePlayersCount) + { + opt.SetInt( + Int32OptionNames.EmergencyCooldown, + Options.AdditionalEmergencyCooldownTime.GetInt()); + } + if (Options.SyncButtonMode.GetBool() && Options.SyncedButtonCount.GetValue() <= Options.UsedButtonCount) + { + opt.SetInt(Int32OptionNames.EmergencyCooldown, 3600); + } + MeetingTimeManager.ApplyGameOptions(opt); + + AURoleOptions.ShapeshifterCooldown = Mathf.Max(1f, AURoleOptions.ShapeshifterCooldown); + AURoleOptions.ProtectionDurationSeconds = 0f; + + return opt; + } + + public override bool AmValid() + { + return base.AmValid() && player != null && !player.Data.Disconnected && Main.RealOptionsData != null; + } +} \ No newline at end of file diff --git a/Modules/GameState.cs b/Modules/GameState.cs new file mode 100644 index 000000000..7b8a9816d --- /dev/null +++ b/Modules/GameState.cs @@ -0,0 +1,561 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using TOHE.Modules; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Neutral; +using UnityEngine; + +namespace TOHE; + +public class PlayerState +{ + byte PlayerId; + public CustomRoles MainRole; + public List SubRoles; + public CountTypes countTypes; + public bool IsDead { get; set; } + public DeathReason deathReason { get; set; } + public TaskState taskState; + public bool IsBlackOut { get; set; } + public (DateTime, byte) RealKiller; + public PlainShipRoom LastRoom; + public Dictionary TargetColorData; + public PlayerState(byte playerId) + { + MainRole = CustomRoles.NotAssigned; + SubRoles = new(); + countTypes = CountTypes.OutOfGame; + PlayerId = playerId; + IsDead = false; + deathReason = DeathReason.etc; + taskState = new(); + IsBlackOut = false; + RealKiller = (DateTime.MinValue, byte.MaxValue); + LastRoom = null; + TargetColorData = new(); + } + public CustomRoles GetCustomRole() + { + var RoleInfo = Utils.GetPlayerInfoById(PlayerId); + return RoleInfo.Role == null + ? MainRole + : RoleInfo.Role.Role switch + { + RoleTypes.Crewmate => CustomRoles.Crewmate, + RoleTypes.Engineer => CustomRoles.Engineer, + RoleTypes.Scientist => CustomRoles.Scientist, + RoleTypes.GuardianAngel => CustomRoles.GuardianAngel, + RoleTypes.Impostor => CustomRoles.Impostor, + RoleTypes.Shapeshifter => CustomRoles.Shapeshifter, + _ => CustomRoles.Crewmate, + }; + } + public void SetMainRole(CustomRoles role) + { + MainRole = role; + countTypes = role.GetCountTypes(); + if (role == CustomRoles.DarkHide) + { + if (!DarkHide.SnatchesWin.GetBool()) + { + countTypes = CountTypes.DarkHide; + } + if (DarkHide.SnatchesWin.GetBool()) + { + countTypes = CountTypes.Crew; + } + } + if (role == CustomRoles.Arsonist) + { + if (Options.ArsonistKeepsGameGoing.GetBool()) + { + countTypes = CountTypes.Arsonist; + } + if (!Options.ArsonistKeepsGameGoing.GetBool()) + { + countTypes = CountTypes.Crew; + } + } + } + public void SetSubRole(CustomRoles role, bool AllReplace = false) + { + if (AllReplace) + SubRoles.ToArray().Do(role => SubRoles.Remove(role)); + + if (!SubRoles.Contains(role)) + SubRoles.Add(role); + + if (role == CustomRoles.Madmate) + { + countTypes = Options.MadmateCountMode.GetInt() switch + { + 0 => CountTypes.OutOfGame, + 1 => CountTypes.Impostor, + 2 => CountTypes.Crew, + _ => throw new NotImplementedException() + }; + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rogue); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + if (role == CustomRoles.Charmed) + { + countTypes = Succubus.CharmedCountMode.GetInt() switch + { + 0 => CountTypes.OutOfGame, + 1 => CountTypes.Succubus, + 2 => countTypes, + _ => throw new NotImplementedException() + }; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rogue); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + if (role == CustomRoles.LastImpostor) + { + SubRoles.Remove(CustomRoles.Mare); + } + if (role == CustomRoles.Recruit) + { + countTypes = Jackal.SidekickCountMode.GetInt() switch + { + 0 => CountTypes.Jackal, + 1 => CountTypes.OutOfGame, + 2 => countTypes, + _ => throw new NotImplementedException() + }; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rogue); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + if (role == CustomRoles.Infected) + { + countTypes = CountTypes.Infectious; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Rogue); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + if (role == CustomRoles.Contagious) + { + countTypes = Virus.ContagiousCountMode.GetInt() switch + { + 0 => CountTypes.OutOfGame, + 1 => CountTypes.Virus, + 2 => countTypes, + _ => throw new NotImplementedException() + }; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Rogue); + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + if (role == CustomRoles.Rogue) + { + countTypes = CountTypes.Rogue; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + if (role == CustomRoles.Admired) + { + countTypes = CountTypes.Crew; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Soulless); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Rogue); + } + if (role == CustomRoles.Soulless) + { + countTypes = CountTypes.OutOfGame; + SubRoles.Remove(CustomRoles.Madmate); + SubRoles.Remove(CustomRoles.Recruit); + SubRoles.Remove(CustomRoles.Charmed); + SubRoles.Remove(CustomRoles.Infected); + SubRoles.Remove(CustomRoles.Contagious); + SubRoles.Remove(CustomRoles.Rascal); + SubRoles.Remove(CustomRoles.Rogue); + SubRoles.Remove(CustomRoles.Loyal); + SubRoles.Remove(CustomRoles.Admired); + } + } + public void RemoveSubRole(CustomRoles role) + { + if (SubRoles.Contains(role)) + SubRoles.Remove(role); + } + + public void SetDead() + { + IsDead = true; + if (AmongUsClient.Instance.AmHost) + { + RPC.SendDeathReason(PlayerId, deathReason); + } + } + public bool IsSuicide() { return deathReason == DeathReason.Suicide; } + public TaskState GetTaskState() { return taskState; } + public void InitTask(PlayerControl player) + { + taskState.Init(player); + } + public void UpdateTask(PlayerControl player) + { + taskState.Update(player); + } + public enum DeathReason + { + Kill, + Vote, + Suicide, + Spell, + Curse, + Hex, + FollowingSuicide, + Bite, + Poison, + Bombed, + Misfire, + Torched, + Sniped, + Revenge, + Execution, + Disconnected, + Fall, + + // TOHE + Gambled, + Eaten, + Sacrifice, + Quantization, + Overtired, + Ashamed, + PissedOff, + Dismembered, + LossOfHead, + Trialed, + Infected, + Jinx, + Hack, + + etc = -1, + } + public byte GetRealKiller() + => IsDead && RealKiller.Item1 != DateTime.MinValue ? RealKiller.Item2 : byte.MaxValue; + public int GetKillCount(bool ExcludeSelfKill = false) + { + int count = 0; + foreach (var state in Main.PlayerStates.Values) + if (!(ExcludeSelfKill && state.PlayerId == PlayerId) && state.GetRealKiller() == PlayerId) + count++; + return count; + } +} +public class TaskState +{ + public static int InitialTotalTasks; + public int AllTasksCount; + public int CompletedTasksCount; + public bool hasTasks; + public int RemainingTasksCount => AllTasksCount - CompletedTasksCount; + public bool DoExpose => RemainingTasksCount <= Options.SnitchExposeTaskLeft && hasTasks; + public bool IsTaskFinished => RemainingTasksCount <= 0 && hasTasks; + public TaskState() + { + this.AllTasksCount = -1; + this.CompletedTasksCount = 0; + this.hasTasks = false; + } + + public void Init(PlayerControl player) + { + Logger.Info($"{player.GetNameWithRole()}: InitTask", "TaskState.Init"); + if (player == null || player.Data == null || player.Data.Tasks == null) return; + if (!Utils.HasTasks(player.Data, false)) + { + AllTasksCount = 0; + return; + } + hasTasks = true; + AllTasksCount = player.Data.Tasks.Count; + Logger.Info($"{player.GetNameWithRole()}: TaskCounts = {CompletedTasksCount}/{AllTasksCount}", "TaskState.Init"); + } + public void Update(PlayerControl player) + { + Logger.Info($"{player.GetNameWithRole()}: UpdateTask", "TaskState.Update"); + GameData.Instance.RecomputeTaskCounts(); + Logger.Info($"TotalTaskCounts = {GameData.Instance.CompletedTasks}/{GameData.Instance.TotalTasks}", "TaskState.Update"); + + //初期化出来ていなかったら初期化 + if (AllTasksCount == -1) Init(player); + + if (!hasTasks) return; + + if (AmongUsClient.Instance.AmHost) + { + //FIXME:SpeedBooster class transplant + if (player.IsAlive() + && player.Is(CustomRoles.SpeedBooster) + && ((CompletedTasksCount + 1) <= Options.SpeedBoosterTimes.GetInt())) + { + Logger.Info("增速者触发加速:" + player.GetNameWithRole(), "SpeedBooster"); + Main.AllPlayerSpeed[player.PlayerId] += Options.SpeedBoosterUpSpeed.GetFloat(); + if (Main.AllPlayerSpeed[player.PlayerId] > 3) player.Notify(Translator.GetString("SpeedBoosterSpeedLimit")); + else player.Notify(string.Format(Translator.GetString("SpeedBoosterTaskDone"), Main.AllPlayerSpeed[player.PlayerId].ToString("0.0#####"))); + } + + + + /* + //叛徒修理搞破坏 + if (player.IsAlive() + && player.Is(CustomRoles.SabotageMaster) + && player.Is(CustomRoles.Madmate)) + { + List SysList = new(); + foreach (SystemTypes sys in Enum.GetValues(typeof(SystemTypes))) + if (Utils.IsActive(sys)) SysList.Add(sys); + + if (SysList.Count > 0) + { + var SbSys = SysList[IRandom.Instance.Next(0, SysList.Count)]; + + MessageWriter SabotageFixWriter = AmongUsClient.Instance.StartRpcImmediately(ShipStatus.Instance.NetId, (byte)RpcCalls.RepairSystem, SendOption.Reliable, player.GetClientId()); + SabotageFixWriter.Write((byte)SbSys); + MessageExtensions.WriteNetObject(SabotageFixWriter, player); + AmongUsClient.Instance.FinishRpcImmediately(SabotageFixWriter); + + foreach (var target in Main.AllPlayerControls) + { + if (target == player || target.Data.Disconnected) continue; + SabotageFixWriter = AmongUsClient.Instance.StartRpcImmediately(ShipStatus.Instance.NetId, (byte)RpcCalls.RepairSystem, SendOption.Reliable, target.GetClientId()); + SabotageFixWriter.Write((byte)SbSys); + MessageExtensions.WriteNetObject(SabotageFixWriter, target); + AmongUsClient.Instance.FinishRpcImmediately(SabotageFixWriter); + } + Logger.Info("叛徒修理工造成破坏:" + player.cosmetics.nameText.text, "SabotageMaster"); + } + } + */ + + //传送师完成任务 + if (player.IsAlive() + && player.Is(CustomRoles.Transporter) + && ((CompletedTasksCount + 1) <= Options.TransporterTeleportMax.GetInt())) + { + Logger.Info("传送师触发传送:" + player.GetNameWithRole(), "Transporter"); + var rd = IRandom.Instance; + List AllAlivePlayer = new(); + foreach (var pc in Main.AllAlivePlayerControls.Where(x => !Pelican.IsEaten(x.PlayerId) && !x.inVent && !x.onLadder)) AllAlivePlayer.Add(pc); + if (AllAlivePlayer.Count >= 2) + { + var tar1 = AllAlivePlayer[rd.Next(0, AllAlivePlayer.Count)]; + AllAlivePlayer.Remove(tar1); + var tar2 = AllAlivePlayer[rd.Next(0, AllAlivePlayer.Count)]; + var pos = tar1.GetTruePosition(); + Utils.TP(tar1.NetTransform, tar2.GetTruePosition()); + Utils.TP(tar2.NetTransform, pos); + tar1.RPCPlayCustomSound("Teleport"); + tar2.RPCPlayCustomSound("Teleport"); + tar1.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Transporter), string.Format(Translator.GetString("TeleportedByTransporter"), tar2.GetRealName()))); + tar2.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Transporter), string.Format(Translator.GetString("TeleportedByTransporter"), tar1.GetRealName()))); + } + else if (player.Is(CustomRoles.Transporter)) + { + player.Notify(Utils.ColorString(Utils.GetRoleColor(CustomRoles.Impostor), string.Format(Translator.GetString("ErrorTeleport"), player.GetRealName()))); + } + } + if (player.Is(CustomRoles.Unlucky) && player.IsAlive()) + { + var Ue = IRandom.Instance; + if (Ue.Next(0, 100) < Options.UnluckyTaskSuicideChance.GetInt()) + { + player.RpcMurderPlayerV3(player); + Main.PlayerStates[player.PlayerId].deathReason = PlayerState.DeathReason.Suicide; + + } + } + + if (player.Is(CustomRoles.Ghoul) && (CompletedTasksCount + 1) >= AllTasksCount && player.IsAlive()) + new LateTask(() => + { + player.RpcMurderPlayerV3(player); + Main.PlayerStates[player.PlayerId].deathReason = PlayerState.DeathReason.Suicide; + }, 0.2f, "Ghoul Suicide"); + if (player.Is(CustomRoles.Ghoul) && (CompletedTasksCount + 1) >= AllTasksCount && !player.IsAlive()) + { + foreach (var pc in Main.AllPlayerControls) + { + if (!pc.Is(CustomRoles.Pestilence)) + { + if (Main.KillGhoul.Contains(pc.PlayerId) && player.PlayerId != pc.PlayerId && pc.IsAlive()) + { + player.RpcMurderPlayerV3(pc); + Main.PlayerStates[pc.PlayerId].deathReason = PlayerState.DeathReason.Kill; + } + } + + } + + } + + //工作狂做完了 + if (player.Is(CustomRoles.Workaholic) && (CompletedTasksCount + 1) >= AllTasksCount + && !(Options.WorkaholicCannotWinAtDeath.GetBool() && !player.IsAlive())) + { + Logger.Info("工作狂任务做完了", "Workaholic"); + RPC.PlaySoundRPC(player.PlayerId, Sounds.KillSound); + foreach (var pc in Main.AllAlivePlayerControls) + { + if (pc.PlayerId != player.PlayerId) + { + Main.PlayerStates[pc.PlayerId].deathReason = pc.PlayerId == player.PlayerId ? + PlayerState.DeathReason.Overtired : PlayerState.DeathReason.Ashamed; + pc.RpcMurderPlayerV3(pc); + Main.PlayerStates[pc.PlayerId].SetDead(); + pc.SetRealKiller(player); + } + } + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Workaholic); //爆破で勝利した人も勝利させる + CustomWinnerHolder.WinnerIds.Add(player.PlayerId); + } + + Merchant.OnTaskFinished(player); + + //船鬼要抽奖啦 + if (player.Is(CustomRoles.Crewpostor)) + { + + List list = Main.AllAlivePlayerControls.Where(x => x.PlayerId != player.PlayerId && (Options.CrewpostorCanKillAllies.GetBool() || !x.GetCustomRole().IsImpostorTeam())).ToList(); + if (list.Count < 1) + { + Logger.Info($"船鬼没有可击杀目标", "Crewpostor"); + } + else + { + { + list = list.OrderBy(x => Vector2.Distance(player.GetTruePosition(), x.GetTruePosition())).ToList(); + var target = list[0]; + if (!target.Is(CustomRoles.Pestilence)) + { + target.SetRealKiller(player); + target.RpcCheckAndMurder(target); + player.RpcGuardAndKill(); + Logger.Info($"船鬼完成任务击杀:{player.GetNameWithRole()} => {target.GetNameWithRole()}", "Crewpostor"); + } + if (target.Is(CustomRoles.Pestilence)) + { + target.SetRealKiller(player); + target.RpcMurderPlayerV3(player); + player.RpcGuardAndKill(); + Logger.Info($"船鬼完成任务击杀:{target.GetNameWithRole()} => {player.GetNameWithRole()}", "Pestilence Reflect"); + } + } + } + } + + } + + //クリアしてたらカウントしない + if (CompletedTasksCount >= AllTasksCount) return; + + CompletedTasksCount++; + + //調整後のタスク量までしか表示しない + CompletedTasksCount = Math.Min(AllTasksCount, CompletedTasksCount); + Logger.Info($"{player.GetNameWithRole()}: TaskCounts = {CompletedTasksCount}/{AllTasksCount}", "TaskState.Update"); + + } +} +public class PlayerVersion +{ + public readonly Version version; + public readonly string tag; + public readonly string forkId; + [Obsolete] public PlayerVersion(string ver, string tag_str) : this(Version.Parse(ver), tag_str, "") { } + [Obsolete] public PlayerVersion(Version ver, string tag_str) : this(ver, tag_str, "") { } + public PlayerVersion(string ver, string tag_str, string forkId) : this(Version.Parse(ver), tag_str, forkId) { } + public PlayerVersion(Version ver, string tag_str, string forkId) + { + version = ver; + tag = tag_str; + this.forkId = forkId; + } + public bool IsEqual(PlayerVersion pv) + { + return pv.version == version && pv.tag == tag; + } +} +public static class GameStates +{ + public static bool InGame = false; + public static bool AlreadyDied = false; + public static bool IsModHost => PlayerControl.AllPlayerControls.ToArray().FirstOrDefault(x => x.PlayerId == 0 && x.IsModClient()); + public static bool IsLobby => AmongUsClient.Instance.GameState == AmongUsClient.GameStates.Joined; + public static bool IsInGame => InGame; + public static bool IsEnded => AmongUsClient.Instance.GameState == AmongUsClient.GameStates.Ended; + public static bool IsNotJoined => AmongUsClient.Instance.GameState == AmongUsClient.GameStates.NotJoined; + public static bool IsOnlineGame => AmongUsClient.Instance.NetworkMode == NetworkModes.OnlineGame; + public static bool IsLocalGame => AmongUsClient.Instance.NetworkMode == NetworkModes.LocalGame; + public static bool IsFreePlay => AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay; + public static bool IsInTask => InGame && !MeetingHud.Instance; + public static bool IsMeeting => InGame && MeetingHud.Instance; + public static bool IsVoting => IsMeeting && MeetingHud.Instance.state is MeetingHud.VoteStates.Voted or MeetingHud.VoteStates.NotVoted; + public static bool IsCountDown => GameStartManager.InstanceExists && GameStartManager.Instance.startState == GameStartManager.StartingStates.Countdown; + /**********TOP ZOOM.cs***********/ + public static bool IsShip => ShipStatus.Instance != null; + public static bool IsCanMove => PlayerControl.LocalPlayer?.CanMove is true; + public static bool IsDead => PlayerControl.LocalPlayer?.Data?.IsDead is true; +} +public static class MeetingStates +{ + public static DeadBody[] DeadBodies = null; + public static GameData.PlayerInfo ReportTarget = null; + public static bool IsEmergencyMeeting => ReportTarget == null; + public static bool IsExistDeadBody => DeadBodies.Length > 0; + public static bool MeetingCalled = false; + public static bool FirstMeeting = true; +} \ No newline at end of file diff --git a/Modules/GuessManager.cs b/Modules/GuessManager.cs new file mode 100644 index 000000000..4055c51d8 --- /dev/null +++ b/Modules/GuessManager.cs @@ -0,0 +1,1249 @@ +using HarmonyLib; +using Hazel; +using TMPro; +using System; +using UnityEngine; +using System.Linq; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using TOHE.Modules; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Neutral; +using TOHE.Roles.AddOns.Common; +using static TOHE.Translator; + +namespace TOHE; + +public static class GuessManager +{ + public static string GetFormatString() + { + string text = GetString("PlayerIdList"); + foreach (var pc in Main.AllAlivePlayerControls) + { + string id = pc.PlayerId.ToString(); + string name = pc.GetRealName(); + text += $"\n{id} → {name}"; + } + return text; + } + + public static bool CheckCommond(ref string msg, string command, bool exact = true) + { + var comList = command.Split('|'); + for (int i = 0; i < comList.Count(); i++) + { + if (exact) + { + if (msg == "/" + comList[i]) return true; + } + else + { + if (msg.StartsWith("/" + comList[i])) + { + msg = msg.Replace("/" + comList[i], string.Empty); + return true; + } + } + } + return false; + } + + public static byte GetColorFromMsg(string msg) + { + if (ComfirmIncludeMsg(msg, "红|紅|red")) return 0; + if (ComfirmIncludeMsg(msg, "蓝|藍|深蓝|blue")) return 1; + if (ComfirmIncludeMsg(msg, "绿|綠|深绿|green")) return 2; + if (ComfirmIncludeMsg(msg, "粉红|粉紅|pink")) return 3; + if (ComfirmIncludeMsg(msg, "橘|橘|orange")) return 4; + if (ComfirmIncludeMsg(msg, "黄|黃|yellow")) return 5; + if (ComfirmIncludeMsg(msg, "黑|黑|black")) return 6; + if (ComfirmIncludeMsg(msg, "白|白|white")) return 7; + if (ComfirmIncludeMsg(msg, "紫|紫|perple")) return 8; + if (ComfirmIncludeMsg(msg, "棕|棕|brown")) return 9; + if (ComfirmIncludeMsg(msg, "青|青|cyan")) return 10; + if (ComfirmIncludeMsg(msg, "黄绿|黃綠|浅绿|lime")) return 11; + if (ComfirmIncludeMsg(msg, "红褐|紅褐|深红|maroon")) return 12; + if (ComfirmIncludeMsg(msg, "玫红|玫紅|浅粉|rose")) return 13; + if (ComfirmIncludeMsg(msg, "焦黄|焦黃|淡黄|banana")) return 14; + if (ComfirmIncludeMsg(msg, "灰|灰|gray")) return 15; + if (ComfirmIncludeMsg(msg, "茶|茶|tan")) return 16; + if (ComfirmIncludeMsg(msg, "珊瑚|珊瑚|coral")) return 17; + else return byte.MaxValue; + } + + private static bool ComfirmIncludeMsg(string msg, string key) + { + var keys = key.Split('|'); + for (int i = 0; i < keys.Count(); i++) + { + if (msg.Contains(keys[i])) return true; + } + return false; + } + + public static bool GuesserMsg(PlayerControl pc, string msg, bool isUI = false) + { + var originMsg = msg; + + if (!AmongUsClient.Instance.AmHost) return false; + if (!GameStates.IsInGame || pc == null) return false; + if (!pc.Is(CustomRoles.NiceGuesser) && !pc.Is(CustomRoles.EvilGuesser) && !pc.Is(CustomRoles.Doomsayer) && !pc.Is(CustomRoles.Judge) && !pc.Is(CustomRoles.Councillor) && !pc.Is(CustomRoles.Guesser) && !Options.GuesserMode.GetBool()) return false; + + int operate = 0; // 1:ID 2:猜测 + msg = msg.ToLower().TrimStart().TrimEnd(); + if (CheckCommond(ref msg, "id|guesslist|gl编号|玩家编号|玩家id|id列表|玩家列表|列表|所有id|全部id")) operate = 1; + else if (CheckCommond(ref msg, "shoot|guess|bet|st|gs|bt|猜|赌", false)) operate = 2; + else return false; + + Logger.Msg(msg, "Msg Guesser"); + Logger.Msg($"{operate}", "Operate"); + + if (!pc.IsAlive()) + { + if (!isUI) Utils.SendMessage(GetString("GuessDead"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDead")); + return true; + } + if (!pc.Is(CustomRoles.NiceGuesser)) + { + if (pc.GetCustomRole().IsCrewmate() && !Options.CrewmatesCanGuess.GetBool() && !pc.Is(CustomRoles.Guesser) && !pc.Is(CustomRoles.Judge)) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + } + if (!pc.Is(CustomRoles.EvilGuesser)) + { + if (pc.GetCustomRole().IsImpostor() && !Options.ImpostorsCanGuess.GetBool() && !pc.Is(CustomRoles.Guesser) && !pc.Is(CustomRoles.Councillor)) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + } + if (pc.GetCustomRole().IsNK() && !Options.NeutralKillersCanGuess.GetBool() && !pc.Is(CustomRoles.Guesser)) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + if (pc.GetCustomRole().IsNonNK() && !Options.PassiveNeutralsCanGuess.GetBool() && !pc.Is(CustomRoles.Guesser) && !pc.Is(CustomRoles.Doomsayer)) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + + if (operate == 1) + { + Utils.SendMessage(GetFormatString(), pc.PlayerId); + return true; + } + else if (operate == 2) + { + + if ( + (pc.Is(CustomRoles.NiceGuesser) && Options.GGTryHideMsg.GetBool()) || + (pc.Is(CustomRoles.EvilGuesser) && Options.EGTryHideMsg.GetBool()) || + (pc.Is(CustomRoles.Doomsayer) && Doomsayer.DoomsayerTryHideMsg.GetBool()) || + (pc.Is(CustomRoles.Guesser) && Options.GTryHideMsg.GetBool() || Options.GuesserMode.GetBool() && Options.HideGuesserCommands.GetBool()) + ) TryHideMsg(); + else if (pc.AmOwner && !isUI) Utils.SendMessage(originMsg, 255, pc.GetRealName()); + + if (!MsgToPlayerAndRole(msg, out byte targetId, out CustomRoles role, out string error)) + { + if (!isUI) Utils.SendMessage(error, pc.PlayerId); + else pc.ShowPopUp(error); + return true; + } + var target = Utils.GetPlayerById(targetId); + if (target != null) + { + bool guesserSuicide = false; + if (CopyCat.playerIdList.Contains(pc.PlayerId)) + { + if (!isUI) Utils.SendMessage(GetString("GuessDisabled"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDisabled")); + return true; + } + + if (!Main.GuesserGuessed.ContainsKey(pc.PlayerId)) Main.GuesserGuessed.Add(pc.PlayerId, 0); + if (pc.Is(CustomRoles.NiceGuesser) && Main.GuesserGuessed[pc.PlayerId] >= Options.GGCanGuessTime.GetInt()) + { + if (!isUI) Utils.SendMessage(GetString("GGGuessMax"), pc.PlayerId); + else pc.ShowPopUp(GetString("GGGuessMax")); + return true; + } + if (pc.Is(CustomRoles.EvilGuesser) && Main.GuesserGuessed[pc.PlayerId] >= Options.EGCanGuessTime.GetInt()) + { + if (!isUI) Utils.SendMessage(GetString("EGGuessMax"), pc.PlayerId); + else pc.ShowPopUp(GetString("EGGuessMax")); + return true; + } + if (pc.Is(CustomRoles.Phantom) && !Options.PhantomCanGuess.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessDisabled"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDisabled")); + return true; + } + if (target.Is(CustomRoles.Workaholic) && Options.WorkaholicVisibleToEveryone.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessWorkaholic"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessWorkaholic")); + return true; + } + if (target.Is(CustomRoles.Doctor) && Options.DoctorVisibleToEveryone.GetBool() && !target.GetCustomRole().IsEvilAddons()) + { + if (!isUI) Utils.SendMessage(GetString("GuessDoctor"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDoctor")); + return true; + } + if (Medic.ProtectList.Contains(target.PlayerId) && !Medic.GuesserIgnoreShield.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessShielded"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessShielded")); + return true; + } + if (role == CustomRoles.Monarch && target.Is(CustomRoles.Monarch) && CustomRoles.Knighted.RoleExist()) + { + if (!isUI) Utils.SendMessage(GetString("GuessMonarch"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessMonarch")); + return true; + } + if (role == CustomRoles.Knighted && pc.Is(CustomRoles.Monarch)) + { + if (!isUI) Utils.SendMessage(GetString("GuessKnighted"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessKnighted")); + return true; + } + if (pc.Is(CustomRoles.Masochist)) + { + if (!isUI) Utils.SendMessage(GetString("GuessMasochistBlocked"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessMasochistBlocked")); + return true; + } + if (Options.MayorRevealWhenDoneTasks.GetBool()) + { + if (target.Is(CustomRoles.Mayor) && target.GetPlayerTaskState().IsTaskFinished) + { + if (!isUI) Utils.SendMessage(GetString("GuessMayor"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessMayor")); + return true; + } + } + if (pc.Is(CustomRoles.Terrorist) && !Options.TerroristCanGuess.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessDisabled"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDisabled")); + return true; + } + if (pc.Is(CustomRoles.Workaholic) && !Options.WorkaholicCanGuess.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessDisabled"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDisabled")); + return true; + } + if (pc.Is(CustomRoles.God) && !Options.GodCanGuess.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessDisabled"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDisabled")); + return true; + } + if (role == CustomRoles.SuperStar || target.Is(CustomRoles.SuperStar)) + { + if (!isUI) Utils.SendMessage(GetString("GuessSuperStar"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessSuperStar")); + return true; + } + if (role == CustomRoles.Bait && target.Is(CustomRoles.Bait) && Options.BaitNotification.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotifiedBait"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotifiedBait")); + return true; + } + if (role == CustomRoles.Glow || role == CustomRoles.LastImpostor || role == CustomRoles.Mare) + { + if (!isUI) Utils.SendMessage(GetString("GuessObviousAddon"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessObviousAddon")); + return true; + } + if (target.Is(CustomRoles.Onbound)) + { + if (!isUI) Utils.SendMessage(GetString("GuessOnbound"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessOnbound")); + return true; + } + if (target.Is(CustomRoles.Pestilence)) + { + if (!isUI) Utils.SendMessage(GetString("GuessPestilence"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessPestilence")); + guesserSuicide = true; + } + if (role == CustomRoles.Phantom && target.Is(CustomRoles.Phantom)) + { + if (!isUI) Utils.SendMessage(GetString("GuessPhantom"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessPhantom")); + return true; + } + if (target.Is(CustomRoles.Masochist)) + { + if (!isUI) Utils.SendMessage(GetString("GuessMasochist"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessMasochist")); + Main.MasochistKillMax[target.PlayerId]++; + + if (Main.MasochistKillMax[target.PlayerId] >= Options.MasochistKillMax.GetInt()) + { + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Masochist); + CustomWinnerHolder.WinnerIds.Add(target.PlayerId); + } + return true; + } + if (pc.Is(CustomRoles.Masochist) && target.PlayerId == pc.PlayerId) + { + if (!isUI) Utils.SendMessage(GetString("SelfGuessMasochist"), pc.PlayerId); + else pc.ShowPopUp(GetString("SelfGuessMasochist")); + guesserSuicide = true; + } + + if (role == CustomRoles.GM || target.Is(CustomRoles.GM)) + { + Utils.SendMessage(GetString("GuessGM"), pc.PlayerId); + return true; + } + /* if (role == CustomRoles.Marshall || target.Is(CustomRoles.Marshall)) + { + Utils.SendMessage(GetString("GuessMarshall"), pc.PlayerId); + return true; + } */ + if (target.Is(CustomRoles.Snitch) && target.GetPlayerTaskState().IsTaskFinished) + { + if (!isUI) Utils.SendMessage(GetString("EGGuessSnitchTaskDone"), pc.PlayerId); + else pc.ShowPopUp(GetString("EGGuessSnitchTaskDone")); + return true; + } + if (target.Is(CustomRoles.Marshall) && target.GetPlayerTaskState().IsTaskFinished) + { + if (!isUI) Utils.SendMessage(GetString("GuessMarshallTask"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessMarshallTask")); + return true; + } + if (role == CustomRoles.Guardian && target.Is(CustomRoles.Guardian) && target.GetPlayerTaskState().IsTaskFinished) + { + if (!isUI) Utils.SendMessage(GetString("GuessGuardianTask"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessGuardianTask")); + return true; + } + if (pc.Is(CustomRoles.Doomsayer)) + { + if (Doomsayer.CantGuess) + { + if (!isUI) Utils.SendMessage(GetString("DoomsayerCantGuess"), pc.PlayerId); + else pc.ShowPopUp(GetString("DoomsayerCantGuess")); + return true; + } + + if (role.IsImpostor() && !Doomsayer.DCanGuessImpostors.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + if (role.IsCrewmate() && !Doomsayer.DCanGuessCrewmates.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + if (role.IsNeutral() && !Doomsayer.DCanGuessNeutrals.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessNotAllowed"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessNotAllowed")); + return true; + } + if (role.IsAdditionRole() && !Doomsayer.DCanGuessAdt.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + } + + // Guesser Mode Can/Cant Guess Addons + if (Options.GuesserMode.GetBool()) + { + // Impostors Cant Guess Addons + if (role.IsAdditionRole() && !Options.CanGuessAddons.GetBool() && (Options.ImpostorsCanGuess.GetBool() && pc.Is(CustomRoleTypes.Impostor))) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Crewmates Cant Guess Addons + if (role.IsAdditionRole() && !Options.CanGuessAddons.GetBool() && (Options.CrewmatesCanGuess.GetBool() && pc.Is(CustomRoleTypes.Crewmate))) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Neutrals Cant Guess Addons + if (role.IsAdditionRole() && !Options.CanGuessAddons.GetBool() && (Options.NeutralKillersCanGuess.GetBool() || Options.PassiveNeutralsCanGuess.GetBool()) && pc.Is(CustomRoleTypes.Neutral)) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Guesser Mode Can Guess Addons + if (Options.CanGuessAddons.GetBool() && (pc.Is(CustomRoles.EvilGuesser) || pc.Is(CustomRoles.NiceGuesser) || pc.Is(CustomRoles.Guesser))) + { + // Assassin Cant Guess Addons + if (role.IsAdditionRole() && (pc.Is(CustomRoles.EvilGuesser) && !Options.EGCanGuessAdt.GetBool())) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Nice Guesser Cant Guess Addons + if (role.IsAdditionRole() && (pc.Is(CustomRoles.NiceGuesser) && !Options.GGCanGuessAdt.GetBool())) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Guesser (add-on) Cant Guess Addons + if (role.IsAdditionRole() && (pc.Is(CustomRoles.Guesser) && !Options.GCanGuessAdt.GetBool())) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + } + } + // Guesser Mode Off, Cant Guess Addon + else + { + // Assassin Cant Guess Addons + if (role.IsAdditionRole() && pc.Is(CustomRoles.EvilGuesser) && !Options.EGCanGuessAdt.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Nice Guesser Cant Guess Addons + else if (role.IsAdditionRole() && pc.Is(CustomRoles.NiceGuesser) && !Options.GGCanGuessAdt.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + + // Guesser (add-on) Cant Guess Addons + else if (role.IsAdditionRole() && pc.Is(CustomRoles.Guesser) && !Options.GCanGuessAdt.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessAdtRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessAdtRole")); + return true; + } + } + + /* if ((pc.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoleTypes.Impostor) && !Options.ImpCanGuessImp.GetBool()) && Options.GuesserMode.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessImpRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessImpRole")); + return true; + + } + if ((role == CustomRoles.Phantom && pc.Is(CustomRoleTypes.Crewmate) && target.Is(CustomRoleTypes.Crewmate) && !Options.CrewCanGuessCrew.GetBool()) && Options.GuesserMode.GetBool()) + { + if (!isUI) Utils.SendMessage(GetString("GuessCrewRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessCrewRole")); + return true; + + } */ + if (target.Is(CustomRoles.Merchant) && Merchant.IsBribedKiller(pc, target)) + { + if (!isUI) Utils.SendMessage(GetString("BribedByMerchant2"), pc.PlayerId); + else pc.ShowPopUp(GetString("BribedByMerchant2")); + return true; + } + + if (pc.PlayerId == target.PlayerId) + { + if (pc.Is(CustomRoles.DoubleShot) && !DoubleShot.IsActive.Contains(pc.PlayerId)) + { + DoubleShot.IsActive.Add(pc.PlayerId); + + Logger.Msg($"{pc.PlayerId}", "GuesserDoubleShotIsActive-1"); + + if (!isUI) Utils.SendMessage(GetString("GuessDoubleShot"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDoubleShot")); + return true; + } + else + { + if (pc.Is(CustomRoles.DoubleShot) && DoubleShot.IsActive.Contains(pc.PlayerId)) + DoubleShot.IsActive.Remove(pc.PlayerId); + + if (!isUI) Utils.SendMessage(GetString("LaughToWhoGuessSelf"), pc.PlayerId, Utils.ColorString(Color.cyan, GetString("MessageFromKPD"))); + else pc.ShowPopUp(Utils.ColorString(Color.cyan, GetString("MessageFromKPD")) + "\n" + GetString("LaughToWhoGuessSelf")); + guesserSuicide = true; + } + } + else if (pc.Is(CustomRoles.NiceGuesser) && target.Is(CustomRoleTypes.Crewmate) && !Options.GGCanGuessCrew.GetBool() && !pc.Is(CustomRoles.Madmate)) + { + if (pc.Is(CustomRoles.DoubleShot) && !DoubleShot.IsActive.Contains(pc.PlayerId)) + { + DoubleShot.IsActive.Add(pc.PlayerId); + + Logger.Msg($"{pc.PlayerId}", "GuesserDoubleShotIsActive-2"); + + if (!isUI) Utils.SendMessage(GetString("GuessDoubleShot"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDoubleShot")); + return true; + } + else + { + if (pc.Is(CustomRoles.DoubleShot) && DoubleShot.IsActive.Contains(pc.PlayerId)) + DoubleShot.IsActive.Remove(pc.PlayerId); + + guesserSuicide = true; + Logger.Msg($"{guesserSuicide}", "guesserSuicide1"); + } + } + else if (pc.Is(CustomRoles.EvilGuesser) && target.Is(CustomRoleTypes.Impostor) && !Options.EGCanGuessImp.GetBool()) + { + if (pc.Is(CustomRoles.DoubleShot) && !DoubleShot.IsActive.Contains(pc.PlayerId)) + { + DoubleShot.IsActive.Add(pc.PlayerId); + + Logger.Msg($"{pc.PlayerId}", "GuesserDoubleShotIsActive-3"); + + if (!isUI) Utils.SendMessage(GetString("GuessDoubleShot"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDoubleShot")); + return true; + } + else + { + if (pc.Is(CustomRoles.DoubleShot) && DoubleShot.IsActive.Contains(pc.PlayerId)) + DoubleShot.IsActive.Remove(pc.PlayerId); + + guesserSuicide = true; + Logger.Msg($"{guesserSuicide}", "guesserSuicide2"); + } + } + // else if (pc.Is(CustomRoles.Guesser)/* && role.IsImpostor() && !Options.GCanGuessImp.GetBool()*/) guesserSuicide = true; + // else if (pc.Is(CustomRoles.Guesser)/* && role.IsCrewmate() && !pc.Is(CustomRoles.Madmate) && !Options.GCanGuessCrew.GetBool() */) guesserSuicide = true; + else if (!target.Is(role)) + { + if (pc.Is(CustomRoles.DoubleShot) && !DoubleShot.IsActive.Contains(pc.PlayerId)) + { + DoubleShot.IsActive.Add(pc.PlayerId); + + Logger.Msg($"{pc.PlayerId}", "GuesserDoubleShotIsActive-4"); + + if (!isUI) Utils.SendMessage(GetString("GuessDoubleShot"), pc.PlayerId); + else pc.ShowPopUp(GetString("GuessDoubleShot")); + return true; + } + else + { + if (pc.Is(CustomRoles.DoubleShot) && DoubleShot.IsActive.Contains(pc.PlayerId)) + DoubleShot.IsActive.Remove(pc.PlayerId); + + guesserSuicide = true; + Logger.Msg($"{guesserSuicide}", "guesserSuicide3"); + } + } + + Logger.Info($"{pc.GetNameWithRole()} guessed {target.GetNameWithRole()}", "Guesser"); + + var dp = guesserSuicide ? pc : target; + target = dp; + + Logger.Info($"Player:{target.GetNameWithRole()} was guessed", "Guesser"); + + Main.GuesserGuessed[pc.PlayerId]++; + + if (pc.Is(CustomRoles.Doomsayer) && Doomsayer.AdvancedSettings.GetBool()) + { + if (Doomsayer.GuessesCountPerMeeting >= Doomsayer.MaxNumberOfGuessesPerMeeting.GetInt() && pc.PlayerId != dp.PlayerId) + { + if (!isUI) Utils.SendMessage(GetString("DoomsayerCantGuess"), pc.PlayerId); + else pc.ShowPopUp(GetString("DoomsayerCantGuess")); + return true; + } + else + { + Doomsayer.GuessesCountPerMeeting++; + + if (Doomsayer.GuessesCountPerMeeting >= Doomsayer.MaxNumberOfGuessesPerMeeting.GetInt()) + Doomsayer.CantGuess = true; + } + + if (!Doomsayer.KillCorrectlyGuessedPlayers.GetBool() && pc.PlayerId != dp.PlayerId) + { + if (!isUI) Utils.SendMessage(GetString("DoomsayerCorrectlyGuessRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("DoomsayerCorrectlyGuessRole")); + + if (Doomsayer.GuessedRoles.Contains(role)) + { + new LateTask(() => + { + Utils.SendMessage(GetString("DoomsayerGuessSameRoleAgainMsg"), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Doomsayer), GetString("DoomsayerGuessCountTitle"))); + }, 0.7f, "Doomsayer Guess Same Role Again Msg"); + } + else + { + Doomsayer.GuessingToWin[pc.PlayerId]++; + Doomsayer.SendRPC(pc); + Doomsayer.GuessedRoles.Add(role); + + new LateTask(() => + { + Utils.SendMessage(string.Format(GetString("DoomsayerGuessCountMsg"), Doomsayer.GuessesCount), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Doomsayer), GetString("DoomsayerGuessCountTitle"))); + }, 0.7f, "Doomsayer Guess Msg 1"); + } + + Doomsayer.CheckCountGuess(pc); + + return true; + } + else if (Doomsayer.DoesNotSuicideWhenMisguessing.GetBool() && pc.PlayerId == dp.PlayerId) + { + if (!isUI) Utils.SendMessage(GetString("DoomsayerNotCorrectlyGuessRole"), pc.PlayerId); + else pc.ShowPopUp(GetString("DoomsayerNotCorrectlyGuessRole")); + + if (Doomsayer.MisguessRolePrevGuessRoleUntilNextMeeting.GetBool()) + { + Doomsayer.CantGuess = true; + } + + return true; + } + } + + string Name = dp.GetRealName(); + CustomSoundsManager.RPCPlayCustomSoundAll("Gunfire"); + + new LateTask(() => + { + Main.PlayerStates[dp.PlayerId].deathReason = PlayerState.DeathReason.Gambled; + dp.SetRealKiller(pc); + RpcGuesserMurderPlayer(dp); + + if (dp.Is(CustomRoles.Medic)) + Medic.IsDead(dp); + + if (pc.Is(CustomRoles.Doomsayer) && pc.PlayerId != dp.PlayerId) + { + Doomsayer.GuessingToWin[pc.PlayerId]++; + Doomsayer.SendRPC(pc); + + if (!Doomsayer.GuessedRoles.Contains(role)) + Doomsayer.GuessedRoles.Add(role); + + Doomsayer.CheckCountGuess(pc); + } + + //死者检查 + Utils.AfterPlayerDeathTasks(dp, true); + + Utils.NotifyRoles(isForMeeting: true, NoCache: true); + + new LateTask(() => { Utils.SendMessage(string.Format(GetString("GuessKill"), Name), 255, Utils.ColorString(Utils.GetRoleColor(CustomRoles.NiceGuesser), GetString("GuessKillTitle"))); }, 0.6f, "Guess Msg"); + + if (pc.Is(CustomRoles.Doomsayer) && pc.PlayerId != dp.PlayerId) + { + new LateTask(() => + { + Utils.SendMessage(string.Format(GetString("DoomsayerGuessCountMsg"), Doomsayer.GuessingToWin[pc.PlayerId]), pc.PlayerId, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Doomsayer), GetString("DoomsayerGuessCountTitle"))); + }, 0.7f, "Doomsayer Guess Msg 2"); + } + + }, 0.2f, "Guesser Kill"); + } + } + return true; + } + + public static TextMeshPro nameText(this PlayerControl p) => p.cosmetics.nameText; + public static TextMeshPro NameText(this PoolablePlayer p) => p.cosmetics.nameText; + public static void RpcGuesserMurderPlayer(this PlayerControl pc, float delay = 0f) //ゲッサー用の殺し方 + { + // DEATH STUFF // + var amOwner = pc.AmOwner; + pc.Data.IsDead = true; + pc.RpcExileV2(); + Main.PlayerStates[pc.PlayerId].SetDead(); + var meetingHud = MeetingHud.Instance; + var hudManager = DestroyableSingleton.Instance; + SoundManager.Instance.PlaySound(pc.KillSfx, false, 0.8f); + hudManager.KillOverlay.ShowKillAnimation(pc.Data, pc.Data); + if (amOwner) + { + hudManager.ShadowQuad.gameObject.SetActive(false); + pc.nameText().GetComponent().material.SetInt("_Mask", 0); + pc.RpcSetScanner(false); + ImportantTextTask importantTextTask = new GameObject("_Player").AddComponent(); + importantTextTask.transform.SetParent(AmongUsClient.Instance.transform, false); + meetingHud.SetForegroundForDead(); + } + PlayerVoteArea voteArea = MeetingHud.Instance.playerStates.First( + x => x.TargetPlayerId == pc.PlayerId + ); + if (voteArea == null) return; + if (voteArea.DidVote) voteArea.UnsetVote(); + voteArea.AmDead = true; + voteArea.Overlay.gameObject.SetActive(true); + voteArea.Overlay.color = Color.white; + voteArea.XMark.gameObject.SetActive(true); + voteArea.XMark.transform.localScale = Vector3.one; + foreach (var playerVoteArea in meetingHud.playerStates) + { + if (playerVoteArea.VotedFor != pc.PlayerId) continue; + playerVoteArea.UnsetVote(); + var voteAreaPlayer = Utils.GetPlayerById(playerVoteArea.TargetPlayerId); + if (!voteAreaPlayer.AmOwner) continue; + meetingHud.ClearVote(); + } + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.GuessKill, SendOption.Reliable, -1); + writer.Write(pc.PlayerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void RpcClientGuess(PlayerControl pc) + { + var amOwner = pc.AmOwner; + var meetingHud = MeetingHud.Instance; + var hudManager = DestroyableSingleton.Instance; + SoundManager.Instance.PlaySound(pc.KillSfx, false, 0.8f); + hudManager.KillOverlay.ShowKillAnimation(pc.Data, pc.Data); + if (amOwner) + { + hudManager.ShadowQuad.gameObject.SetActive(false); + pc.nameText().GetComponent().material.SetInt("_Mask", 0); + pc.RpcSetScanner(false); + ImportantTextTask importantTextTask = new GameObject("_Player").AddComponent(); + importantTextTask.transform.SetParent(AmongUsClient.Instance.transform, false); + meetingHud.SetForegroundForDead(); + } + PlayerVoteArea voteArea = MeetingHud.Instance.playerStates.First( + x => x.TargetPlayerId == pc.PlayerId + ); + //pc.Die(DeathReason.Kill); + if (voteArea == null) return; + if (voteArea.DidVote) voteArea.UnsetVote(); + voteArea.AmDead = true; + voteArea.Overlay.gameObject.SetActive(true); + voteArea.Overlay.color = Color.white; + voteArea.XMark.gameObject.SetActive(true); + voteArea.XMark.transform.localScale = Vector3.one; + foreach (var playerVoteArea in meetingHud.playerStates) + { + if (playerVoteArea.VotedFor != pc.PlayerId) continue; + playerVoteArea.UnsetVote(); + var voteAreaPlayer = Utils.GetPlayerById(playerVoteArea.TargetPlayerId); + if (!voteAreaPlayer.AmOwner) continue; + meetingHud.ClearVote(); + } + } + private static bool MsgToPlayerAndRole(string msg, out byte id, out CustomRoles role, out string error) + { + if (msg.StartsWith("/")) msg = msg.Replace("/", string.Empty); + + Regex r = new("\\d+"); + MatchCollection mc = r.Matches(msg); + string result = string.Empty; + for (int i = 0; i < mc.Count; i++) + { + result += mc[i];//匹配结果是完整的数字,此处可以不做拼接的 + } + + if (int.TryParse(result, out int num)) + { + id = Convert.ToByte(num); + } + else + { + //并不是玩家编号,判断是否颜色 + //byte color = GetColorFromMsg(msg); + //好吧我不知道怎么取某位玩家的颜色,等会了的时候再来把这里补上 + id = byte.MaxValue; + error = GetString("GuessHelp"); + role = new(); + return false; + } + + //判断选择的玩家是否合理 + PlayerControl target = Utils.GetPlayerById(id); + if (target == null || target.Data.IsDead) + { + error = GetString("GuessNull"); + role = new(); + return false; + } + + if (!ChatCommands.GetRoleByName(msg, out role)) + { + error = GetString("GuessHelp"); + return false; + } + + error = string.Empty; + return true; + } + + public static void TryHideMsg() + { + ChatUpdatePatch.DoBlockChat = true; + List roles = Enum.GetValues(typeof(CustomRoles)).Cast().Where(x => x is not CustomRoles.NotAssigned and not CustomRoles.KB_Normal).ToList(); + var rd = IRandom.Instance; + string msg; + string[] command = new string[] { "bet", "bt", "guess", "gs", "shoot", "st", "赌", "猜", "审判", "tl", "判", "审" }; + for (int i = 0; i < 20; i++) + { + msg = "/"; + if (rd.Next(1, 100) < 20) + { + msg += "id"; + } + else + { + msg += command[rd.Next(0, command.Length - 1)]; + msg += rd.Next(1, 100) < 50 ? string.Empty : " "; + msg += rd.Next(0, 15).ToString(); + msg += rd.Next(1, 100) < 50 ? string.Empty : " "; + CustomRoles role = roles[rd.Next(0, roles.Count())]; + msg += rd.Next(1, 100) < 50 ? string.Empty : " "; + msg += Utils.GetRoleName(role); + } + var player = Main.AllAlivePlayerControls.ToArray()[rd.Next(0, Main.AllAlivePlayerControls.Count())]; + DestroyableSingleton.Instance.Chat.AddChat(player, msg); + var writer = CustomRpcSender.Create("MessagesToSend", SendOption.None); + writer.StartMessage(-1); + writer.StartRpc(player.NetId, (byte)RpcCalls.SendChat) + .Write(msg) + .EndRpc(); + writer.EndMessage(); + writer.SendMessage(); + } + ChatUpdatePatch.DoBlockChat = false; + } + + [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] + class StartMeetingPatch + { + public static void Postfix(MeetingHud __instance) + { + + /*if (!Options.GuesserMode.GetBool()) + { + foreach (var subRole in PlayerControl.LocalPlayer.GetCustomSubRoles()) + { + switch (subRole) + { + + case CustomRoles.Guesser: + { + if (PlayerControl.LocalPlayer.IsAlive()) + CreateGuesserButton(__instance); + } + break; + } + } + }*/ + + if (Options.GuesserMode.GetBool()) + { + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.GetCustomRole().IsImpostor() && Options.ImpostorsCanGuess.GetBool()) + CreateGuesserButton(__instance); + else if (PlayerControl.LocalPlayer.GetCustomRole() is CustomRoles.EvilGuesser && !Options.ImpostorsCanGuess.GetBool()) + CreateGuesserButton(__instance); + + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.GetCustomRole().IsCrewmate() && Options.CrewmatesCanGuess.GetBool()) + CreateGuesserButton(__instance); + else if (PlayerControl.LocalPlayer.GetCustomRole() is CustomRoles.NiceGuesser && !Options.CrewmatesCanGuess.GetBool()) + CreateGuesserButton(__instance); + + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.GetCustomRole().IsNK() && Options.NeutralKillersCanGuess.GetBool()) + CreateGuesserButton(__instance); + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.GetCustomRole().IsNonNK() && Options.PassiveNeutralsCanGuess.GetBool()) + CreateGuesserButton(__instance); + else if (PlayerControl.LocalPlayer.GetCustomRole() is CustomRoles.Doomsayer && !Options.PassiveNeutralsCanGuess.GetBool() && !Doomsayer.CantGuess) + CreateGuesserButton(__instance); + } + else + { + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.Is(CustomRoles.EvilGuesser)) + CreateGuesserButton(__instance); + + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.Is(CustomRoles.NiceGuesser)) + CreateGuesserButton(__instance); + + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.Is(CustomRoles.Doomsayer) && !Doomsayer.CantGuess) + CreateGuesserButton(__instance); + + if (PlayerControl.LocalPlayer.IsAlive() && PlayerControl.LocalPlayer.Is(CustomRoles.Guesser)) + CreateGuesserButton(__instance); + } + } + } + public static void CreateGuesserButton(MeetingHud __instance) + { + + foreach (var pva in __instance.playerStates) + { + var pc = Utils.GetPlayerById(pva.TargetPlayerId); + if (pc == null || !pc.IsAlive()) continue; + GameObject template = pva.Buttons.transform.Find("CancelButton").gameObject; + GameObject targetBox = UnityEngine.Object.Instantiate(template, pva.transform); + targetBox.name = "ShootButton"; + targetBox.transform.localPosition = new Vector3(-0.95f, 0.03f, -1.31f); + SpriteRenderer renderer = targetBox.GetComponent(); + renderer.sprite = CustomButton.Get("TargetIcon"); + PassiveButton button = targetBox.GetComponent(); + button.OnClick.RemoveAllListeners(); + button.OnClick.AddListener((Action)(() => GuesserOnClick(pva.TargetPlayerId, __instance))); + } + } + + public const int MaxOneScreenRole = 40; + public static int Page; + public static PassiveButton ExitButton; + public static GameObject guesserUI; + private static Dictionary> RoleButtons; + private static Dictionary RoleSelectButtons; + private static List PageButtons; + public static CustomRoleTypes currentTeamType; + static void GuesserSelectRole(CustomRoleTypes Role, bool SetPage = true) + { + currentTeamType = Role; + if (SetPage) Page = 1; + foreach (var RoleButton in RoleButtons) + { + int index = 0; + foreach (var RoleBtn in RoleButton.Value) + { + if (RoleBtn == null) continue; + index++; + if (index <= (Page - 1) * 40) { RoleBtn.gameObject.SetActive(false); continue; } + if ((Page * 40) < index) { RoleBtn.gameObject.SetActive(false); continue; } + RoleBtn.gameObject.SetActive(RoleButton.Key == Role); + } + } + foreach (var RoleButton in RoleSelectButtons) + { + if (RoleButton.Value == null) continue; + RoleButton.Value.color = new(0, 0, 0, RoleButton.Key == Role ? 1 : 0.25f); + } + } + + public static TextMeshPro textTemplate; + static void GuesserOnClick(byte playerId, MeetingHud __instance) + { + var pc = Utils.GetPlayerById(playerId); + if (pc == null || !pc.IsAlive() || guesserUI != null || !GameStates.IsVoting) return; + + try + { + Page = 1; + RoleButtons = new(); + RoleSelectButtons = new(); + PageButtons = new(); + __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(false)); + + Transform container = UnityEngine.Object.Instantiate(GameObject.Find("PhoneUI").transform, __instance.transform); + container.transform.localPosition = new Vector3(0, 0, -200f); + guesserUI = container.gameObject; + + List i = new() { 0, 0, 0, 0 }; + var buttonTemplate = __instance.playerStates[0].transform.FindChild("votePlayerBase"); + var maskTemplate = __instance.playerStates[0].transform.FindChild("MaskArea"); + var smallButtonTemplate = __instance.playerStates[0].Buttons.transform.Find("CancelButton"); + textTemplate.enabled = true; + if (textTemplate.transform.FindChild("RoleTextMeeting") != null) UnityEngine.Object.Destroy(textTemplate.transform.FindChild("RoleTextMeeting").gameObject); + + Transform exitButtonParent = new GameObject().transform; + exitButtonParent.SetParent(container); + Transform exitButton = UnityEngine.Object.Instantiate(buttonTemplate, exitButtonParent); + exitButton.FindChild("ControllerHighlight").gameObject.SetActive(false); + Transform exitButtonMask = UnityEngine.Object.Instantiate(maskTemplate, exitButtonParent); + exitButtonMask.transform.localScale = new Vector3(2.88f, 0.8f, 1f); + exitButtonMask.transform.localPosition = new Vector3(0f, 0f, 1f); + exitButton.gameObject.GetComponent().sprite = smallButtonTemplate.GetComponent().sprite; + exitButtonParent.transform.localPosition = new Vector3(3.88f, 2.12f, -200f); + exitButtonParent.transform.localScale = new Vector3(0.22f, 0.9f, 1f); + exitButtonParent.transform.SetAsFirstSibling(); + exitButton.GetComponent().OnClick.RemoveAllListeners(); + exitButton.GetComponent().OnClick.AddListener((Action)(() => + { + __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true)); + UnityEngine.Object.Destroy(container.gameObject); + })); + ExitButton = exitButton.GetComponent(); + + List buttons = new(); + Transform selectedButton = null; + + int tabCount = 0; + for (int index = 0; index < 4; index++) + { + if (PlayerControl.LocalPlayer.Is(CustomRoles.EvilGuesser)) + { + if (!Options.EGCanGuessImp.GetBool() && index == 1) continue; + if (!Options.EGCanGuessAdt.GetBool() && index == 3) continue; + } + else if (PlayerControl.LocalPlayer.Is(CustomRoles.NiceGuesser)) + { + if (!Options.GGCanGuessCrew.GetBool() && index == 0) continue; + if (!Options.GGCanGuessAdt.GetBool() && index == 3) continue; + } + else if (PlayerControl.LocalPlayer.Is(CustomRoles.Doomsayer)) + { + if (!Doomsayer.DCanGuessCrewmates.GetBool() && index == 0) continue; + if (!Doomsayer.DCanGuessImpostors.GetBool() && index == 1) continue; + if (!Doomsayer.DCanGuessNeutrals.GetBool() && index == 2) continue; + if (!Doomsayer.DCanGuessAdt.GetBool() && index == 3) continue; + } + else if (PlayerControl.LocalPlayer.Is(CustomRoles.Guesser)) + { + if (!Options.GCanGuessCrew.GetBool() && PlayerControl.LocalPlayer.Is(CustomRoleTypes.Crewmate) && index == 0) continue; + if (!Options.GCanGuessImp.GetBool() && PlayerControl.LocalPlayer.Is(CustomRoleTypes.Impostor) && index == 1) continue; + if (!Options.GCanGuessAdt.GetBool() && index == 3) continue; + } + else if (Options.GuesserMode.GetBool()) + { + if (!Options.CrewCanGuessCrew.GetBool() && PlayerControl.LocalPlayer.Is(CustomRoleTypes.Crewmate) && index == 0) continue; + if (!Options.ImpCanGuessImp.GetBool() && PlayerControl.LocalPlayer.Is(CustomRoleTypes.Impostor) && index == 1) continue; + // if (index == 2) continue; + if (!Options.CanGuessAddons.GetBool() && index == 3) continue; + + } + Transform TeambuttonParent = new GameObject().transform; + TeambuttonParent.SetParent(container); + Transform Teambutton = UnityEngine.Object.Instantiate(buttonTemplate, TeambuttonParent); + Teambutton.FindChild("ControllerHighlight").gameObject.SetActive(false); + Transform TeambuttonMask = UnityEngine.Object.Instantiate(maskTemplate, TeambuttonParent); + TextMeshPro Teamlabel = UnityEngine.Object.Instantiate(textTemplate, Teambutton); + Teambutton.GetComponent().sprite = CustomButton.Get("GuessPlateWithKPD"); + RoleSelectButtons.Add((CustomRoleTypes)index, Teambutton.GetComponent()); + TeambuttonParent.localPosition = new(-2.75f + (tabCount++ * 1.73f), 2.225f, -200); + TeambuttonParent.localScale = new(0.53f, 0.53f, 1f); + Teamlabel.color = (CustomRoleTypes)index switch + { + CustomRoleTypes.Crewmate => new Color32(140, 255, 255, byte.MaxValue), + CustomRoleTypes.Impostor => new Color32(255, 25, 25, byte.MaxValue), + CustomRoleTypes.Neutral => new Color32(127, 140, 141, byte.MaxValue), + // CustomRoleTypes.Madmate => new Color32(255, 25, 25, byte.MaxValue), + CustomRoleTypes.Addon => new Color32(255, 154, 206, byte.MaxValue), + _ => throw new NotImplementedException(), + }; + Logger.Info(Teamlabel.color.ToString(), ((CustomRoleTypes)index).ToString()); + Teamlabel.text = GetString("Type" + ((CustomRoleTypes)index).ToString()); + Teamlabel.alignment = TextAlignmentOptions.Center; + Teamlabel.transform.localPosition = new Vector3(0, 0, Teamlabel.transform.localPosition.z); + Teamlabel.transform.localScale *= 1.6f; + Teamlabel.autoSizeTextContainer = true; + + static void CreateTeamButton(Transform Teambutton, CustomRoleTypes type) + { + Teambutton.GetComponent().OnClick.AddListener((UnityEngine.Events.UnityAction)(() => + { + GuesserSelectRole(type); + ReloadPage(); + })); + } + if (PlayerControl.LocalPlayer.IsAlive()) CreateTeamButton(Teambutton, (CustomRoleTypes)index); + } + static void ReloadPage() + { + PageButtons[0].color = new(1, 1, 1, 1f); + PageButtons[1].color = new(1, 1, 1, 1f); + if ((RoleButtons[currentTeamType].Count / MaxOneScreenRole + (RoleButtons[currentTeamType].Count % MaxOneScreenRole != 0 ? 1 : 0)) < Page) + { + Page -= 1; + PageButtons[1].color = new(1, 1, 1, 0.1f); + } + else if ((RoleButtons[currentTeamType].Count / MaxOneScreenRole + (RoleButtons[currentTeamType].Count % MaxOneScreenRole != 0 ? 1 : 0)) < Page + 1) + { + PageButtons[1].color = new(1, 1, 1, 0.1f); + } + if (Page <= 1) + { + Page = 1; + PageButtons[0].color = new(1, 1, 1, 0.1f); + } + GuesserSelectRole(currentTeamType, false); + } + static void CreatePage(bool IsNext, MeetingHud __instance, Transform container) + { + var buttonTemplate = __instance.playerStates[0].transform.FindChild("votePlayerBase"); + var maskTemplate = __instance.playerStates[0].transform.FindChild("MaskArea"); + var smallButtonTemplate = __instance.playerStates[0].Buttons.transform.Find("CancelButton"); + Transform PagebuttonParent = new GameObject().transform; + PagebuttonParent.SetParent(container); + Transform Pagebutton = UnityEngine.Object.Instantiate(buttonTemplate, PagebuttonParent); + Pagebutton.FindChild("ControllerHighlight").gameObject.SetActive(false); + Transform PagebuttonMask = UnityEngine.Object.Instantiate(maskTemplate, PagebuttonParent); + TextMeshPro Pagelabel = UnityEngine.Object.Instantiate(textTemplate, Pagebutton); + Pagebutton.GetComponent().sprite = CustomButton.Get("GuessPlateWithKPD"); + PagebuttonParent.localPosition = IsNext ? new(3.535f, -2.2f, -200) : new(-3.475f, -2.2f, -200); + PagebuttonParent.localScale = new(0.55f, 0.55f, 1f); + Pagelabel.color = Color.white; + Pagelabel.text = GetString(IsNext ? "NextPage" : "PreviousPage"); + Pagelabel.alignment = TextAlignmentOptions.Center; + Pagelabel.transform.localPosition = new Vector3(0, 0, Pagelabel.transform.localPosition.z); + Pagelabel.transform.localScale *= 1.6f; + Pagelabel.autoSizeTextContainer = true; + if (!IsNext && Page <= 1) Pagebutton.GetComponent().color = new(1, 1, 1, 0.1f); + Pagebutton.GetComponent().OnClick.AddListener((Action)(() => ClickEvent())); + void ClickEvent() + { + if (IsNext) Page += 1; + else Page -= 1; + if (Page < 1) Page = 1; + ReloadPage(); + } + PageButtons.Add(Pagebutton.GetComponent()); + } + if (PlayerControl.LocalPlayer.IsAlive()) + { + CreatePage(false, __instance, container); + CreatePage(true, __instance, container); + } + int ind = 0; + foreach (CustomRoles role in Enum.GetValues(typeof(CustomRoles))) + { + if ( role is CustomRoles.GM + or CustomRoles.SpeedBooster + or CustomRoles.Engineer + or CustomRoles.Crewmate + // or CustomRoles.Loyal + or CustomRoles.Oblivious + or CustomRoles.Baker + or CustomRoles.Famine + or CustomRoles.Rogue + or CustomRoles.Scientist + or CustomRoles.Impostor + or CustomRoles.Shapeshifter + or CustomRoles.Flashman + or CustomRoles.NotAssigned + or CustomRoles.KB_Normal + // or CustomRoles.Marshall + or CustomRoles.Paranoia + or CustomRoles.SuperStar + or CustomRoles.Konan + or CustomRoles.Oblivious + // or CustomRoles.Reflective + or CustomRoles.GuardianAngelTOHE + ) continue; + + CreateRole(role); + } + void CreateRole(CustomRoles role) + { + if (40 <= i[(int)role.GetCustomRoleTypes()]) i[(int)role.GetCustomRoleTypes()] = 0; + Transform buttonParent = new GameObject().transform; + buttonParent.SetParent(container); + Transform button = UnityEngine.Object.Instantiate(buttonTemplate, buttonParent); + button.FindChild("ControllerHighlight").gameObject.SetActive(false); + Transform buttonMask = UnityEngine.Object.Instantiate(maskTemplate, buttonParent); + TextMeshPro label = UnityEngine.Object.Instantiate(textTemplate, button); + button.GetComponent().sprite = CustomButton.Get("GuessPlate"); + if (!RoleButtons.ContainsKey(role.GetCustomRoleTypes())) + { + RoleButtons.Add(role.GetCustomRoleTypes(), new()); + } + RoleButtons[role.GetCustomRoleTypes()].Add(button); + buttons.Add(button); + int row = i[(int)role.GetCustomRoleTypes()] / 5; + int col = i[(int)role.GetCustomRoleTypes()] % 5; + buttonParent.localPosition = new Vector3(-3.47f + 1.75f * col, 1.5f - 0.45f * row, -200f); + buttonParent.localScale = new Vector3(0.55f, 0.55f, 1f); + label.text = GetString(role.ToString()); + label.color = Utils.GetRoleColor(role); + label.alignment = TextAlignmentOptions.Center; + label.transform.localPosition = new Vector3(0, 0, label.transform.localPosition.z); + label.transform.localScale *= 1.6f; + label.autoSizeTextContainer = true; + int copiedIndex = i[(int)role.GetCustomRoleTypes()]; + + button.GetComponent().OnClick.RemoveAllListeners(); + if (PlayerControl.LocalPlayer.IsAlive()) button.GetComponent().OnClick.AddListener((Action)(() => + { + if (selectedButton != button) + { + selectedButton = button; + buttons.ForEach(x => x.GetComponent().color = x == selectedButton ? Utils.GetRoleColor(PlayerControl.LocalPlayer.GetCustomRole()) : Color.white); + } + else + { + if (!(__instance.state == MeetingHud.VoteStates.Voted || __instance.state == MeetingHud.VoteStates.NotVoted) || !PlayerControl.LocalPlayer.IsAlive()) return; + + Logger.Msg($"Click: {pc.GetNameWithRole()} => {role}", "Guesser UI"); + + if (AmongUsClient.Instance.AmHost) GuesserMsg(PlayerControl.LocalPlayer, $"/bt {playerId} {GetString(role.ToString())}", true); + else SendRPC(playerId, role); + + // Reset the GUI + __instance.playerStates.ToList().ForEach(x => x.gameObject.SetActive(true)); + UnityEngine.Object.Destroy(container.gameObject); + textTemplate.enabled = false; + + } + })); + i[(int)role.GetCustomRoleTypes()]++; + ind++; + } + container.transform.localScale *= 0.75f; + GuesserSelectRole(CustomRoleTypes.Crewmate); + ReloadPage(); + } + catch (Exception ex) + { + Logger.Exception(ex, "Guesser UI"); + return; + } + + PlayerControl.LocalPlayer.RPCPlayCustomSound("Gunload"); + + } + + [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.OnDestroy))] + class MeetingHudOnDestroyGuesserUIClose + { + public static void Postfix() + { + UnityEngine.Object.Destroy(textTemplate.gameObject); + } + } + + // Modded non-host client guess role/add-on + private static void SendRPC(int playerId, CustomRoles role) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (int)CustomRPC.Guess, SendOption.Reliable, -1); + writer.Write(playerId); + writer.Write((int)role); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPC(MessageReader reader, PlayerControl pc) + { + Logger.Msg($"{reader}", "MessageReader reader"); + Logger.Msg($"{pc}", "PlayerControl pc"); + + int PlayerId = reader.ReadInt32(); + Logger.Msg($"{PlayerId}", "Player Id"); + + CustomRoles role = (CustomRoles)reader.ReadInt32(); + Logger.Msg($"{role}", "Role Int32"); + Logger.Msg($"{GetString(role.ToString())}", "Role String"); + + GuesserMsg(pc, $"/bt {PlayerId} {GetString(role.ToString())}", true); + } +} \ No newline at end of file diff --git a/Modules/HashAuth.cs b/Modules/HashAuth.cs new file mode 100644 index 000000000..d5fccb437 --- /dev/null +++ b/Modules/HashAuth.cs @@ -0,0 +1,66 @@ +using System.Security.Cryptography; +using System.Text; + +namespace TOHE; + +public class HashAuth +{ + public readonly string HashValue; + + private readonly string salt; + private HashAlgorithm algorithm; + public HashAuth(string hashValue, string salt = null, HashAlgorithm algorithm = null) + { + HashValue = hashValue; + this.salt = salt; + this.algorithm = algorithm ?? SHA256.Create(); // 引数のalgorithmがnullの場合のみ新しく作る + } + + public bool CheckString(string value) + { + var hash = CalculateHash(value); + return HashValue == hash; + } + public string CalculateHash(string source) + => HashAuth.CalculateHash(source, salt, algorithm); + + public static string CalculateHash(string source, string salt = null, HashAlgorithm algorithm = null) + { + // 0.algorithmの初期化 + algorithm ??= SHA256.Create(); + + // 1.saltの適用 + if (salt != null) source += salt; + + // 2.sourceをbyte配列に変換 + var sourceBytes = Encoding.UTF8.GetBytes(source); + + // 3.sourceBytesをハッシュ化 + var hashBytes = algorithm.ComputeHash(sourceBytes); + + // 4.hashBytesを文字列化 + var sb = new StringBuilder(); + foreach (var b in hashBytes) + sb.Append(b.ToString("x2")); //1byteずつ2桁の16進法表記に変換する + + return sb.ToString(); + } + + // Hash値確認用 Hash化してからインスタンスを生成 + // あくまでHash値の確認と動作テストを同時に行うためのものです。確認後は使用しないでください。 + public static HashAuth CreateByUnhashedValue(string value, string salt = null) + { + // 1.ハッシュ値計算 + var algorithm = SHA256.Create(); + string hashValue = CalculateHash(value, salt, algorithm); + + // 2.ハッシュ値のログ出力 + // salt有: ハッシュ値算出結果: => (salt: ) + // salt無: ハッシュ値算出結果: => + Logger.Info($"ハッシュ値算出結果: {value} => {hashValue} {(salt == null ? "" : $"(salt: {salt})")}", "HashAuth"); + Logger.Warn("以上の値をソースコード上にペーストしてください。", "HashAuth"); + + // 3.HashAuthインスタンスの生成・リターン + return new HashAuth(hashValue, salt, algorithm); + } +} \ No newline at end of file diff --git a/Modules/LateTask.cs b/Modules/LateTask.cs new file mode 100644 index 000000000..97a1defc8 --- /dev/null +++ b/Modules/LateTask.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace TOHE; + +class LateTask +{ + public string name; + public float timer; + public Action action; + public static List Tasks = new(); + public bool Run(float deltaTime) + { + timer -= deltaTime; + if (timer <= 0) + { + action(); + return true; + } + return false; + } + public LateTask(Action action, float time, string name = "No Name Task") + { + this.action = action; + this.timer = time; + this.name = name; + Tasks.Add(this); + if (name != "") + Logger.Info("\"" + name + "\" is created", "LateTask"); + } + public static void Update(float deltaTime) + { + var TasksToRemove = new List(); + for (int i = 0; i < Tasks.Count; i++) + { + var task = Tasks[i]; + try + { + if (task.Run(deltaTime)) + { + if (task.name != "") + Logger.Info($"\"{task.name}\" is finished", "LateTask"); + TasksToRemove.Add(task); + } + } + catch (Exception ex) + { + Logger.Error($"{ex.GetType()}: {ex.Message} in \"{task.name}\"\n{ex.StackTrace}", "LateTask.Error", false); + TasksToRemove.Add(task); + } + } + TasksToRemove.ForEach(task => Tasks.Remove(task)); + } +} \ No newline at end of file diff --git a/Modules/LocateArrow.cs b/Modules/LocateArrow.cs new file mode 100644 index 000000000..f3af793c8 --- /dev/null +++ b/Modules/LocateArrow.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace TOHE; + +static class LocateArrow +{ + class ArrowInfo + { + public byte From; + public Vector3 To; + public ArrowInfo(byte from, Vector3 to) + { + From = from; + To = to; + } + public bool Equals(ArrowInfo obj) + { + return From == obj.From && To == obj.To; + } + public override string ToString() + { + return $"(From:{From} To:{To})"; + } + } + + static Dictionary LocateArrows = new(); + static readonly string[] Arrows = { + "↑", + "↗", + "→", + "↘", + "↓", + "↙", + "←", + "↖", + "・" + }; + + public static void Init() + { + LocateArrows.Clear(); + } + /// + /// 新たにターゲット矢印対象を登録 + /// + /// + /// + /// + public static void Add(byte seer, Vector3 locate) + { + var arrowInfo = new ArrowInfo(seer, locate); + if (!LocateArrows.Any(a => a.Key.Equals(arrowInfo))) + LocateArrows[arrowInfo] = "・"; + } + /// + /// ターゲットの削除 + /// + /// + /// + public static void Remove(byte seer, Vector3 locate) + { + var arrowInfo = new ArrowInfo(seer, locate); + var removeList = new List(LocateArrows.Keys.Where(k => k.Equals(arrowInfo))); + foreach (var a in removeList) + { + LocateArrows.Remove(a); + } + } + /// + /// タイプの同じターゲットの全削除 + /// + /// + public static void RemoveAllTarget(byte seer) + { + var removeList = new List(LocateArrows.Keys.Where(k => k.From == seer)); + foreach (var arrowInfo in removeList) + { + LocateArrows.Remove(arrowInfo); + } + } + /// + /// 見ることのできるすべてのターゲット矢印を取得 + /// + /// + /// + public static string GetArrows(PlayerControl seer) + { + var arrows = ""; + foreach (var arrowInfo in LocateArrows.Keys.Where(ai => ai.From == seer.PlayerId)) + { + arrows += LocateArrows[arrowInfo]; + } + return arrows; + } + /// + /// FixedUpdate毎にターゲット矢印を確認 + /// 更新があったらNotifyRolesを発行 + /// + /// + public static void OnFixedUpdate(PlayerControl seer) + { + if (!GameStates.IsInTask) return; + + var seerId = seer.PlayerId; + var seerIsDead = !seer.IsAlive(); + + var arrowList = new List(LocateArrows.Keys.Where(a => a.From == seer.PlayerId)); + if (arrowList.Count == 0) return; + + var update = false; + foreach (var arrowInfo in arrowList) + { + var loc = arrowInfo.To; + if (seerIsDead) + { + LocateArrows.Remove(arrowInfo); + update = true; + continue; + } + //対象の方角ベクトルを取る + var dir = loc - seer.transform.position; + int index; + if (dir.magnitude < 2) + { + //近い時はドット表示 + index = 8; + } + else + { + //-22.5~22.5度を0とするindexに変換 + // 下が0度、左側が+180まで右側が-180まで + // 180度足すことで上が0度の時計回り + // 45度単位のindexにするため45/2を加算 + var angle = Vector3.SignedAngle(Vector3.down, dir, Vector3.back) + 180 + 22.5; + index = ((int)(angle / 45)) % 8; + } + var arrow = Arrows[index]; + if (LocateArrows[arrowInfo] != arrow) + { + LocateArrows[arrowInfo] = arrow; + update = true; + } + } + if (update) + { + Utils.NotifyRoles(SpecifySeer: seer); + } + } +} diff --git a/Modules/LogHandler/ILogHandler.cs b/Modules/LogHandler/ILogHandler.cs new file mode 100644 index 000000000..fc0597f74 --- /dev/null +++ b/Modules/LogHandler/ILogHandler.cs @@ -0,0 +1,13 @@ +using System; + +namespace TOHE.Modules; + +public interface ILogHandler +{ + public void Info(string text); + public void Warn(string text); + public void Error(string text); + public void Fatal(string text); + public void Msg(string text); + public void Exception(Exception ex); +} \ No newline at end of file diff --git a/Modules/LogHandler/LogHandler.cs b/Modules/LogHandler/LogHandler.cs new file mode 100644 index 000000000..c3f341d45 --- /dev/null +++ b/Modules/LogHandler/LogHandler.cs @@ -0,0 +1,25 @@ +using System; + +namespace TOHE.Modules; + +class LogHandler : ILogHandler +{ + public string Tag { get; } + public LogHandler(string tag) + { + Tag = tag; + } + + public void Info(string text) + => Logger.Info(text, Tag, true); + public void Warn(string text) + => Logger.Warn(text, Tag, true); + public void Error(string text) + => Logger.Error(text, Tag, true); + public void Fatal(string text) + => Logger.Fatal(text, Tag, true); + public void Msg(string text) + => Logger.Msg(text, Tag, true); + public void Exception(Exception ex) + => Logger.Exception(ex, Tag); +} \ No newline at end of file diff --git a/Modules/MafiaRevengeManager.cs b/Modules/MafiaRevengeManager.cs new file mode 100644 index 000000000..4843bd062 --- /dev/null +++ b/Modules/MafiaRevengeManager.cs @@ -0,0 +1,164 @@ +using HarmonyLib; +using Hazel; +using Rewired.UI.ControlMapper; +using System; +using TOHE.Modules; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +public static class MafiaRevengeManager +{ + public static bool MafiaMsgCheck(PlayerControl pc, string msg, bool isUI = false) + { + if (!AmongUsClient.Instance.AmHost) return false; + if (!GameStates.IsInGame || pc == null) return false; + if (!pc.Is(CustomRoles.Mafia)) return false; + msg = msg.Trim().ToLower(); + if (msg.Length < 3 || msg[..3] != "/rv") return false; + if (Options.MafiaCanKillNum.GetInt() < 1) + { + if (!isUI) Utils.SendMessage(GetString("MafiaKillDisable"), pc.PlayerId); + else pc.ShowPopUp(GetString("MafiaKillDisable")); + return true; + } + + if (!pc.Data.IsDead) + { + Utils.SendMessage(GetString("MafiaAliveKill"), pc.PlayerId); + return true; + } + + if (msg == "/rv") + { + string text = GetString("PlayerIdList"); + foreach (var npc in Main.AllAlivePlayerControls) + text += "\n" + npc.PlayerId.ToString() + " → (" + npc.GetDisplayRoleName() + ") " + npc.GetRealName(); + Utils.SendMessage(text, pc.PlayerId); + return true; + } + + if (Main.MafiaRevenged.ContainsKey(pc.PlayerId)) + { + if (Main.MafiaRevenged[pc.PlayerId] >= Options.MafiaCanKillNum.GetInt()) + { + if (!isUI) Utils.SendMessage(GetString("MafiaKillMax"), pc.PlayerId); + else pc.ShowPopUp(GetString("MafiaKillMax")); + return true; + } + } + else + { + Main.MafiaRevenged.Add(pc.PlayerId, 0); + } + + int targetId; + PlayerControl target; + try + { + targetId = int.Parse(msg.Replace("/rv", string.Empty)); + target = Utils.GetPlayerById(targetId); + } + catch + { + if (!isUI) Utils.SendMessage(GetString("MafiaKillDead"), pc.PlayerId); + else pc.ShowPopUp(GetString("MafiaKillDead")); + return true; + } + + if (target == null || target.Data.IsDead) + { + if (!isUI) Utils.SendMessage(GetString("MafiaKillDead"), pc.PlayerId); + else pc.ShowPopUp(GetString("MafiaKillDead")); + return true; + } + if (target.Is(CustomRoles.Pestilence)) + { + if (!isUI) Utils.SendMessage(GetString("PestilenceImmune"), pc.PlayerId); + else pc.ShowPopUp(GetString("PestilenceImmune")); + return true; + } + + Logger.Info($"{pc.GetNameWithRole()} 复仇了 {target.GetNameWithRole()}", "Mafia"); + + string Name = target.GetRealName(); + + Main.MafiaRevenged[pc.PlayerId]++; + + CustomSoundsManager.RPCPlayCustomSoundAll("AWP"); + + new LateTask(() => + { + Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Revenge; + target.SetRealKiller(pc); + + if (GameStates.IsMeeting) + { + GuessManager.RpcGuesserMurderPlayer(target); + + //死者检查 + Utils.AfterPlayerDeathTasks(target, true); + + Utils.NotifyRoles(isForMeeting: false, NoCache: true); + } + else + { + target.RpcMurderPlayerV3(target); + Main.PlayerStates[target.PlayerId].SetDead(); + } + + new LateTask(() => { Utils.SendMessage(string.Format(GetString("MafiaKillSucceed"), Name), 255, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Mafia), GetString("MafiaRevengeTitle"))); }, 0.6f, "Mafia Kill"); + + }, 0.2f, "Mafia Kill"); + return true; + } + + private static void SendRPC(byte playerId) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.MafiaRevenge, SendOption.Reliable, -1); + writer.Write(playerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPC(MessageReader reader, PlayerControl pc) + { + int PlayerId = reader.ReadByte(); + MafiaMsgCheck(pc, $"/rv {PlayerId}", true); + } + + private static void MafiaOnClick(byte playerId, MeetingHud __instance) + { + Logger.Msg($"Click: ID {playerId}", "Mafia UI"); + var pc = Utils.GetPlayerById(playerId); + if (pc == null || !pc.IsAlive() || !GameStates.IsVoting) return; + if (AmongUsClient.Instance.AmHost) MafiaMsgCheck(PlayerControl.LocalPlayer, $"/rv {playerId}", true); + else SendRPC(playerId); + } + + [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] + class StartMeetingPatch + { + public static void Postfix(MeetingHud __instance) + { + if (PlayerControl.LocalPlayer.Is(CustomRoles.Mafia) && !PlayerControl.LocalPlayer.IsAlive()) + CreateJudgeButton(__instance); + } + } + public static void CreateJudgeButton(MeetingHud __instance) + { + foreach (var pva in __instance.playerStates) + { + var pc = Utils.GetPlayerById(pva.TargetPlayerId); + if (pc == null || !pc.IsAlive()) continue; + GameObject template = pva.Buttons.transform.Find("CancelButton").gameObject; + GameObject targetBox = UnityEngine.Object.Instantiate(template, pva.transform); + targetBox.name = "ShootButton"; + targetBox.transform.localPosition = new Vector3(-0.95f, 0.03f, -1.31f); + SpriteRenderer renderer = targetBox.GetComponent(); + renderer.sprite = CustomButton.Get("TargetIcon"); + PassiveButton button = targetBox.GetComponent(); + button.OnClick.RemoveAllListeners(); + button.OnClick.AddListener((Action)(() => MafiaOnClick(pva.TargetPlayerId, __instance))); + } + } +} diff --git a/Modules/MeetingTimeManager.cs b/Modules/MeetingTimeManager.cs new file mode 100644 index 000000000..eff4c387c --- /dev/null +++ b/Modules/MeetingTimeManager.cs @@ -0,0 +1,79 @@ +using AmongUs.GameOptions; +using System; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Impostor; + +namespace TOHE.Modules; + +public class MeetingTimeManager +{ + private static int DiscussionTime; + private static int VotingTime; + private static int DefaultDiscussionTime; + private static int DefaultVotingTime; + + public static void Init() + { + DefaultDiscussionTime = Main.RealOptionsData.GetInt(Int32OptionNames.DiscussionTime); + DefaultVotingTime = Main.RealOptionsData.GetInt(Int32OptionNames.VotingTime); + Logger.Info($"DefaultDiscussionTime:{DefaultDiscussionTime}, DefaultVotingTime{DefaultVotingTime}", "MeetingTimeManager.Init"); + ResetMeetingTime(); + } + public static void ApplyGameOptions(IGameOptions opt) + { + opt.SetInt(Int32OptionNames.DiscussionTime, DiscussionTime); + opt.SetInt(Int32OptionNames.VotingTime, VotingTime); + } + private static void ResetMeetingTime() + { + DiscussionTime = DefaultDiscussionTime; + VotingTime = DefaultVotingTime; + } + public static void OnReportDeadBody() + { + if (Options.AllAliveMeeting.GetBool() && Utils.IsAllAlive) + { + DiscussionTime = 0; + VotingTime = Options.AllAliveMeetingTime.GetInt(); + Logger.Info($"DiscussionTime:{DiscussionTime}, VotingTime{VotingTime}", "MeetingTimeManager.OnReportDeadBody"); + return; + } + + ResetMeetingTime(); + int BonusMeetingTime = 0; + int MeetingTimeMinTimeThief = 0; + int MeetingTimeMinTimeManager = 0; + int MeetingTimeMax = 300; + + if (TimeThief.IsEnable) + { + MeetingTimeMinTimeThief = TimeThief.LowerLimitVotingTime.GetInt(); + BonusMeetingTime += TimeThief.TotalDecreasedMeetingTime(); + } + if (TimeManager.IsEnable) + { + MeetingTimeMinTimeManager = TimeManager.MadMinMeetingTimeLimit.GetInt(); + MeetingTimeMax = TimeManager.MeetingTimeLimit.GetInt(); + BonusMeetingTime += TimeManager.TotalIncreasedMeetingTime(); + } + + int TotalMeetingTime = DiscussionTime + VotingTime; + //時間の下限、上限で刈り込み + if (TimeManager.IsEnable) BonusMeetingTime = Math.Clamp(TotalMeetingTime + BonusMeetingTime, MeetingTimeMinTimeManager, MeetingTimeMax) - TotalMeetingTime; + if (TimeThief.IsEnable) BonusMeetingTime = Math.Clamp(TotalMeetingTime + BonusMeetingTime, MeetingTimeMinTimeThief, MeetingTimeMax) - TotalMeetingTime; + if (!TimeManager.IsEnable && !TimeThief.IsEnable) BonusMeetingTime = Math.Clamp(TotalMeetingTime + BonusMeetingTime, MeetingTimeMinTimeThief, MeetingTimeMax) - TotalMeetingTime; + + if (BonusMeetingTime >= 0) + VotingTime += BonusMeetingTime; //投票時間を延長 + else + { + DiscussionTime += BonusMeetingTime; //会議時間を優先的に短縮 + if (DiscussionTime < 0) //会議時間だけでは賄えない場合 + { + VotingTime += DiscussionTime; //足りない分投票時間を短縮 + DiscussionTime = 0; + } + } + Logger.Info($"DiscussionTime:{DiscussionTime}, VotingTime{VotingTime}", "MeetingTimeManager.OnReportDeadBody"); + } +} \ No newline at end of file diff --git a/Modules/MessageManager/ICustomWriter.cs b/Modules/MessageManager/ICustomWriter.cs new file mode 100644 index 000000000..8582ff2b1 --- /dev/null +++ b/Modules/MessageManager/ICustomWriter.cs @@ -0,0 +1,23 @@ +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; + +namespace TOHE; + +public interface ICustomWriter +{ + public void Write(float val); + public void Write(string val); + public void Write(ulong val); + public void Write(int val); + public void Write(uint val); + public void Write(ushort val); + public void Write(byte val); + public void Write(sbyte val); + public void Write(bool val); + public void Write(Il2CppStructArray bytes); + public void Write(Il2CppStructArray bytes, int offset, int length); + public void WriteBytesAndSize(Il2CppStructArray bytes); + public void WritePacked(int val); + public void WritePacked(uint val); + public void WriteNetObject(InnerNetObject obj); +} \ No newline at end of file diff --git a/Modules/MessageManager/WriterProxy.cs b/Modules/MessageManager/WriterProxy.cs new file mode 100644 index 000000000..38b8c02a6 --- /dev/null +++ b/Modules/MessageManager/WriterProxy.cs @@ -0,0 +1,31 @@ +using Hazel; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; + +namespace TOHE; + +public class WriterProxy : ICustomWriter +{ + public MessageWriter Writer { get; private set; } + + public WriterProxy(MessageWriter writer) + { + this.Writer = writer; + } + + public void Write(float val) => Writer.Write(val); + public void Write(string val) => Writer.Write(val); + public void Write(ulong val) => Writer.Write(val); + public void Write(int val) => Writer.Write(val); + public void Write(uint val) => Writer.Write(val); + public void Write(ushort val) => Writer.Write(val); + public void Write(byte val) => Writer.Write(val); + public void Write(sbyte val) => Writer.Write(val); + public void Write(bool val) => Writer.Write(val); + public void Write(Il2CppStructArray bytes) => Writer.Write(bytes); + public void Write(Il2CppStructArray bytes, int offset, int length) => Writer.Write(bytes, offset, length); + public void WriteBytesAndSize(Il2CppStructArray bytes) => Writer.WriteBytesAndSize(bytes); + public void WritePacked(int val) => Writer.WritePacked(val); + public void WritePacked(uint val) => Writer.WritePacked(val); + public void WriteNetObject(InnerNetObject obj) => Writer.WriteNetObject(obj); +} \ No newline at end of file diff --git a/Modules/ModUpdater.cs b/Modules/ModUpdater.cs new file mode 100644 index 000000000..d3f163080 --- /dev/null +++ b/Modules/ModUpdater.cs @@ -0,0 +1,395 @@ +using HarmonyLib; +//using Newtonsoft.Json.Linq; +using System; +//using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +[HarmonyPatch] +public class ModUpdater +{ + //private static readonly string URL_2018k = "http://api.2018k.cn"; + //private static readonly string URL_Github = "https://api.github.com/repos/Loonie-Toons/TOHE-Restored"; + public static bool hasUpdate = false; + public static bool forceUpdate = false; + public static bool isBroken = false; + public static bool isChecked = false; + public static Version latestVersion = null; + public static string latestTitle = null; + public static string downloadUrl = null; + public static string md5 = null; + public static string notice = null; + public static GenericPopup InfoPopup; + public static int visit = 0; + + [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start)), HarmonyPrefix] + [HarmonyPriority(2)] + public static void Start_Prefix(MainMenuManager __instance) + { + /*NewVersionCheck(); + DeleteOldFiles(); + InfoPopup = UnityEngine.Object.Instantiate(Twitch.TwitchManager.Instance.TwitchPopup); + InfoPopup.name = "InfoPopup"; + InfoPopup.TextAreaTMP.GetComponent().sizeDelta = new(2.5f, 2f); + if (!isChecked) + { + var done = false; + if (CultureInfo.CurrentCulture.Name == "zh-CN") + { + done = CheckRelease().GetAwaiter().GetResult(); + } + else + { + done = CheckReleaseFromGithub(Main.BetaBuildURL.Value != "").GetAwaiter().GetResult(); + done = CheckRelease(done).GetAwaiter().GetResult(); + } + Logger.Warn("检查更新结果: " + done, "CheckRelease"); + Logger.Info("hasupdate: " + hasUpdate, "CheckRelease"); + Logger.Info("forceupdate: " + forceUpdate, "CheckRelease"); + Logger.Info("downloadUrl: " + downloadUrl, "CheckRelease"); + Logger.Info("latestVersionl: " + latestVersion, "CheckRelease"); + }*/ + + } + + public static string UrlSetId(string url) => url + "?id=6C5A46D1420E476ABD560271FC8040D7"; + public static string UrlSetCheck(string url) => url + "/checkVersion"; + public static string UrlSetInfo(string url) => url + "/getExample"; + public static string UrlSetToday(string url) => url + "/today"; + + public static string Get(string url) + { + string result = ""; + HttpClient req = new HttpClient(); + var res = req.GetAsync(url).Result; + Stream stream = res.Content.ReadAsStreamAsync().Result; + try + { + //获取内容 + using StreamReader reader = new(stream); + result = reader.ReadToEnd(); + } + finally + { + stream.Close(); + } + return result; + } + + /*public static Task CheckRelease(bool onlyInfo = false) + { + Logger.Warn("开始从2018k检查更新", "CheckRelease"); + string url = UrlSetId(UrlSetCheck(URL_2018k)) + "&version=" + Main.PluginVersion; + try + { + string res = Get(url); + string[] info = res.Split("|"); + if (!onlyInfo) + { + hasUpdate = false; + forceUpdate = info[1] == "false"; + latestVersion = new(info[4]); + latestTitle = "Ver. " + info[4]; + + string[] num = info[4].Split("."); + string[] inum = Main.PluginVersion.Split("."); + if (num.Length > inum.Length) inum.AddItem("0"); + for (int i = 0; i < num.Length; i++) + { + int c = int.Parse(num[i]); + int m = int.Parse(inum[i]); + if (c > m) hasUpdate = true; + if (c != m) break; + } + } + + +#if DEBUG + if (!hasUpdate && Main.PluginVersion == info[4] && !onlyInfo) hasUpdate = true; +#endif + + + if (downloadUrl == null || downloadUrl == "") + { + Logger.Error("获取下载地址失败", "CheckRelease"); + return Task.FromResult(false); + } + + isChecked = true; + isBroken = false; + } + catch (Exception ex) + { + if (CultureInfo.CurrentCulture.Name == "zh-CN") + { + isChecked = false; + isBroken = true; + } + else if (!onlyInfo) + { + isChecked = true; + isBroken = false; + Logger.Error($"检查更新时发生错误\n{ex}", "CheckRelease", false); + } + Logger.Error($"检查更新时发生错误,已忽略\n{ex}", "CheckRelease", false); + return Task.FromResult(false); + } + return Task.FromResult(true); + } + public static async Task CheckReleaseFromGithub(bool beta = false) + { + Logger.Warn("开始从Github检查更新", "CheckRelease"); + string url = beta ? Main.BetaBuildURL.Value : URL_Github + "/releases/latest"; + try + { + string result; + using (HttpClient client = new()) + { + client.DefaultRequestHeaders.Add("User-Agent", "TOHE Updater"); + using var response = await client.GetAsync(new Uri(url), HttpCompletionOption.ResponseContentRead); + if (!response.IsSuccessStatusCode || response.Content == null) + { + Logger.Error($"状态码: {response.StatusCode}", "CheckRelease"); + return false; + } + result = await response.Content.ReadAsStringAsync(); + } + JObject data = JObject.Parse(result); + if (beta) + { + latestTitle = data["name"].ToString(); + downloadUrl = data["url"].ToString(); + hasUpdate = latestTitle != ThisAssembly.Git.Commit; + } + else + { + latestVersion = new(data["tag_name"]?.ToString().TrimStart('v')); + latestTitle = $"Ver. {latestVersion}"; + JArray assets = data["assets"].Cast(); + for (int i = 0; i < assets.Count; i++) + { + if (assets[i]["name"].ToString() == "TOHE_Steam.dll" && Constants.GetPlatformType() == Platforms.StandaloneSteamPC) + { + downloadUrl = assets[i]["browser_download_url"].ToString(); + break; + } + if (assets[i]["name"].ToString() == "TOHE_Epic.dll" && Constants.GetPlatformType() == Platforms.StandaloneEpicPC) + { + downloadUrl = assets[i]["browser_download_url"].ToString(); + break; + } + if (assets[i]["name"].ToString() == "TOHE.dll") + downloadUrl = assets[i]["browser_download_url"].ToString(); + } + hasUpdate = latestVersion.CompareTo(Main.version) > 0; + } + + Logger.Info("hasupdate: " + hasUpdate, "Github"); + Logger.Info("forceupdate: " + forceUpdate, "Github"); + Logger.Info("downloadUrl: " + downloadUrl, "Github"); + Logger.Info("latestVersionl: " + latestVersion, "Github"); + Logger.Info("latestTitle: " + latestTitle, "Github"); + + if (downloadUrl == null || downloadUrl == "") + { + Logger.Error("获取下载地址失败", "CheckRelease"); + return false; + } + isChecked = true; + isBroken = false; + } + catch (Exception ex) + { + isBroken = true; + Logger.Error($"发布检查失败\n{ex}", "CheckRelease", false); + return false; + } + return true; + }*/ + public static void StartUpdate(string url) + { + ShowPopup(GetString("updatePleaseWait"), StringNames.Cancel, true, false); + _ = DownloadDLL(url); + return; + } + public static bool NewVersionCheck() + { + try + { + var fileName = Assembly.GetExecutingAssembly().Location; + if (Directory.Exists("TOH_DATA") && File.Exists(@"./TOHE_DATA/BanWords.txt")) + { + DirectoryInfo di = new("TOH_DATA"); + di.Delete(true); + Logger.Warn("删除旧数据:TOH_DATA", "NewVersionCheck"); + } + } + catch (Exception ex) + { + Logger.Exception(ex, "NewVersionCheck"); + return false; + } + return true; + } + /*public static bool BackOldDLL() + { + try + { + foreach (var path in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "*.dll")) + { + Logger.Info($"{Path.GetFileName(path)} 已删除", "BackOldDLL"); + File.Delete(path); + } + File.Move(Assembly.GetExecutingAssembly().Location + ".bak", Assembly.GetExecutingAssembly().Location); + } + catch + { + Logger.Error("回退老版本失败", "BackOldDLL"); + return false; + } + return true; + } + public static void DeleteOldFiles() + { + try + { + foreach (var path in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "*.*")) + { + if (path.EndsWith(Path.GetFileName(Assembly.GetExecutingAssembly().Location))) continue; + if (path.EndsWith("TOHE.dll")) continue; + Logger.Info($"{Path.GetFileName(path)} 已删除", "DeleteOldFiles"); + File.Delete(path); + } + } + catch (Exception e) + { + Logger.Error($"清除更新残留失败\n{e}", "DeleteOldFiles"); + } + return; + }*/ + private static readonly object downloadLock = new(); + public static async Task DownloadDLL(string url) + { + try + { + var savePath = "BepInEx/plugins/TOHE.dll.temp"; + File.Delete(savePath); + +#nullable enable + HttpResponseMessage? response = null; +#nullable disable + var downloadCallBack = DownloadCallBack; + using (HttpClient client = new HttpClient()) + response = await client.GetAsync(url); + if (response == null) + throw new Exception("文件获取失败"); + var total = response.Content.Headers.ContentLength ?? 0; + var stream = await response.Content.ReadAsStreamAsync(); + var file = new FileInfo(savePath); + using (var fileStream = file.Create()) + using (stream) + { + if (downloadCallBack == null) + { + await stream.CopyToAsync(fileStream); + } + else + { + byte[] buffer = new byte[1024]; + long readLength = 0; + int length; + while ((length = await stream.ReadAsync(buffer, 0, buffer.Length)) != 0) + { + // 写入到文件 + fileStream.Write(buffer, 0, length); + + //更新进度 + readLength += length; + double? progress = Math.Round((double)readLength / total * 100, 2, MidpointRounding.ToZero); + lock (downloadLock) + { + //下载完毕立刻关闭释放文件流 + if (total == readLength && progress == 100) + { + fileStream.Close(); + fileStream.Dispose(); + } + downloadCallBack?.Invoke(total, readLength, progress ?? 0); + } + } + } + } + + if (GetMD5HashFromFile(savePath) != md5) + { + File.Delete(savePath); + ShowPopup(GetString("downloadFailed"), StringNames.Okay, true, false); + MainMenuManagerPatch.updateButton.SetActive(true); + MainMenuManagerPatch.updateButton.transform.position = MainMenuManagerPatch.template.transform.position + new Vector3(0.25f, 0.75f); + } + else + { + var fileName = Assembly.GetExecutingAssembly().Location; + File.Move(fileName, fileName + ".bak"); + File.Move("BepInEx/plugins/TOHE.dll.temp", fileName); + ShowPopup(GetString("updateRestart"), StringNames.ExitGame, true, true); + } + } + catch (Exception ex) + { + Logger.Error($"更新失败\n{ex}", "DownloadDLL", false); + ShowPopup(GetString("updateManually"), StringNames.ExitGame, true, true); + return false; + } + return true; + } + public static string GetMD5HashFromFile(string fileName) + { + try + { + FileStream file = new(fileName, FileMode.Open); + MD5 md5 = MD5.Create(); + byte[] retVal = md5.ComputeHash(file); + file.Close(); + + StringBuilder sb = new(); + for (int i = 0; i < retVal.Length; i++) + { + sb.Append(retVal[i].ToString("x2")); + } + return sb.ToString(); + } + catch (Exception ex) + { + throw new Exception("GetMD5HashFromFile() fail,error:" + ex.Message); + } + } + private static void DownloadCallBack(long total, long downloaded, double progress) + { + ShowPopup($"{GetString("updateInProgress")}\n{downloaded}/{total}({progress}%)", StringNames.Cancel, true, false); + } + private static void ShowPopup(string message, StringNames buttonText, bool showButton = false, bool buttonIsExit = true) + { + if (InfoPopup != null) + { + InfoPopup.Show(message); + var button = InfoPopup.transform.FindChild("ExitGame"); + if (button != null) + { + button.gameObject.SetActive(showButton); + button.GetChild(0).GetComponent().TargetText = buttonText; + button.GetComponent().OnClick = new(); + if (buttonIsExit) button.GetComponent().OnClick.AddListener((Action)(() => Application.Quit())); + else button.GetComponent().OnClick.AddListener((Action)(() => InfoPopup.Close())); + } + } + } +} \ No newline at end of file diff --git a/Modules/NameColorManager.cs b/Modules/NameColorManager.cs new file mode 100644 index 000000000..292959d19 --- /dev/null +++ b/Modules/NameColorManager.cs @@ -0,0 +1,243 @@ +using Hazel; +using TOHE.Roles.Impostor; +using TOHE.Roles.Neutral; + +namespace TOHE; + +public static class NameColorManager +{ + public static string ApplyNameColorData(this string name, PlayerControl seer, PlayerControl target, bool isMeeting) + { + if (!AmongUsClient.Instance.IsGameStarted) return name; + + if (!TryGetData(seer, target, out var colorCode)) + { + if (KnowTargetRoleColor(seer, target, isMeeting, out var color)) + colorCode = color == "" ? target.GetRoleColorCode() : color; + } + string openTag = "", closeTag = ""; + if (colorCode != "") + { + if (!colorCode.StartsWith('#')) + colorCode = "#" + colorCode; + openTag = $""; + closeTag = ""; + } + return openTag + name + closeTag; + } + private static bool KnowTargetRoleColor(PlayerControl seer, PlayerControl target, bool isMeeting, out string color) + { + color = ""; + + // �ڹ���ͽ���� + if (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoleTypes.Impostor)) color = (target.Is(CustomRoles.Egoist) && Options.ImpEgoistVisibalToAllies.GetBool() && seer != target) ? Main.roleColors[CustomRoles.Egoist] : Main.roleColors[CustomRoles.Impostor]; + // if (seer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick)) color = Main.roleColors[CustomRoles.Jackal]; + // if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Jackal)) color = Main.roleColors[CustomRoles.Jackal]; + // if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick) && Options.SidekickKnowOtherSidekick.GetBool()) color = Main.roleColors[CustomRoles.Sidekick]; + // if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick) && Options.SidekickKnowOtherSidekick.GetBool() && Options.SidekickKnowOtherSidekickRole.GetBool()) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Madmate) && target.Is(CustomRoleTypes.Impostor) && Options.MadmateKnowWhosImp.GetBool()) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Crewpostor) && target.Is(CustomRoleTypes.Impostor) && Options.CrewpostorKnowsAllies.GetBool()) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoles.Madmate) && Options.ImpKnowWhosMadmate.GetBool()) color = Main.roleColors[CustomRoles.Madmate]; + if (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoles.Crewpostor) && Options.AlliesKnowCrewpostor.GetBool()) color = Main.roleColors[CustomRoles.Madmate]; + if (seer.Is(CustomRoles.Madmate) && target.Is(CustomRoles.Madmate) && Options.MadmateKnowWhosMadmate.GetBool()) color = Main.roleColors[CustomRoles.Madmate]; + // if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick) && Options.SidekickKnowOtherSidekick.GetBool() && Options.SidekickKnowOtherSidekickRole.GetBool()) color = Main.roleColors[CustomRoles.Sidekick]; + if (seer.Is(CustomRoles.Gangster) && target.Is(CustomRoles.Madmate)) color = Main.roleColors[CustomRoles.Madmate]; + + //��ħС�ܻ��� + if (seer.Is(CustomRoles.Charmed) && target.Is(CustomRoles.Succubus)) color = Main.roleColors[CustomRoles.Succubus]; + if (seer.Is(CustomRoles.Succubus) && target.Is(CustomRoles.Charmed)) color = Main.roleColors[CustomRoles.Charmed]; + if (seer.Is(CustomRoles.Charmed) && target.Is(CustomRoles.Charmed) && Succubus.TargetKnowOtherTarget.GetBool()) color = Main.roleColors[CustomRoles.Charmed]; + + // Cursed Soul + if (seer.Is(CustomRoles.CursedSoul) && (target.Is(CustomRoles.Soulless))) color = Main.roleColors[CustomRoles.Soulless]; + + // Admirer + if (seer.Is(CustomRoles.Admirer) && (target.Is(CustomRoles.Admired))) color = Main.roleColors[CustomRoles.Admirer]; + if (seer.Is(CustomRoles.Admired) && (target.Is(CustomRoles.Admirer))) color = Main.roleColors[CustomRoles.Admirer]; + + // Amnesiac + if (seer.Is(CustomRoles.Jackal) && (target.Is(CustomRoles.Jackal))) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Juggernaut) && (target.Is(CustomRoles.Juggernaut))) color = Main.roleColors[CustomRoles.Juggernaut]; + if (seer.Is(CustomRoles.NSerialKiller) && (target.Is(CustomRoles.NSerialKiller))) color = Main.roleColors[CustomRoles.NSerialKiller]; + if (seer.Is(CustomRoles.NWitch) && (target.Is(CustomRoles.NWitch))) color = Main.roleColors[CustomRoles.NWitch]; + if (seer.Is(CustomRoles.Jinx) && (target.Is(CustomRoles.Jinx))) color = Main.roleColors[CustomRoles.Jinx]; + if (seer.Is(CustomRoles.Wraith) && (target.Is(CustomRoles.Wraith))) color = Main.roleColors[CustomRoles.Wraith]; + if (seer.Is(CustomRoles.HexMaster) && (target.Is(CustomRoles.HexMaster))) color = Main.roleColors[CustomRoles.HexMaster]; + if (seer.Is(CustomRoles.BloodKnight) && (target.Is(CustomRoles.BloodKnight))) color = Main.roleColors[CustomRoles.BloodKnight]; + if (seer.Is(CustomRoles.Pelican) && (target.Is(CustomRoles.Pelican))) color = Main.roleColors[CustomRoles.Pelican]; + if (seer.Is(CustomRoles.Poisoner) && (target.Is(CustomRoles.Poisoner))) color = Main.roleColors[CustomRoles.Poisoner]; + if (seer.Is(CustomRoles.Infectious) && (target.Is(CustomRoles.Infectious))) color = Main.roleColors[CustomRoles.Infectious]; + if (seer.Is(CustomRoles.Virus) && (target.Is(CustomRoles.Virus))) color = Main.roleColors[CustomRoles.Virus]; + if (seer.Is(CustomRoles.Parasite) && (target.Is(CustomRoles.Parasite))) color = Main.roleColors[CustomRoles.Parasite]; + if (seer.Is(CustomRoles.Traitor) && (target.Is(CustomRoles.Traitor))) color = Main.roleColors[CustomRoles.Traitor]; + if (seer.Is(CustomRoles.DarkHide) && (target.Is(CustomRoles.DarkHide))) color = Main.roleColors[CustomRoles.DarkHide]; + if (seer.Is(CustomRoles.Pickpocket) && (target.Is(CustomRoles.Pickpocket))) color = Main.roleColors[CustomRoles.Pickpocket]; + if (seer.Is(CustomRoles.Spiritcaller) && (target.Is(CustomRoles.Spiritcaller))) color = Main.roleColors[CustomRoles.Spiritcaller]; + if (seer.Is(CustomRoles.Medusa) && (target.Is(CustomRoles.Medusa))) color = Main.roleColors[CustomRoles.Medusa]; + if (seer.Is(CustomRoles.Ritualist) && (target.Is(CustomRoles.Ritualist))) color = Main.roleColors[CustomRoles.Ritualist]; + if (seer.Is(CustomRoles.Glitch) && (target.Is(CustomRoles.Glitch))) color = Main.roleColors[CustomRoles.Glitch]; + if (seer.Is(CustomRoles.Succubus) && (target.Is(CustomRoles.Succubus))) color = Main.roleColors[CustomRoles.Succubus]; + + if (seer.Is(CustomRoles.Refugee) && (target.Is(CustomRoleTypes.Impostor))) color = Main.roleColors[CustomRoles.ImpostorTOHE]; + if (seer.Is(CustomRoleTypes.Impostor) && (target.Is(CustomRoles.Refugee))) color = Main.roleColors[CustomRoles.Refugee]; + + // Infectious + if (seer.Is(CustomRoles.Infected) && target.Is(CustomRoles.Infectious)) color = Main.roleColors[CustomRoles.Infectious]; + if (seer.Is(CustomRoles.Infectious) && target.Is(CustomRoles.Infected)) color = Main.roleColors[CustomRoles.Infected]; + if (seer.Is(CustomRoles.Infected) && target.Is(CustomRoles.Infected) && Infectious.TargetKnowOtherTarget.GetBool()) color = Main.roleColors[CustomRoles.Infectious]; + + // Necroview + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoleTypes.Impostor) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Madmate) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Admired) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Bait]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Parasite) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Crewpostor) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Convict) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Refugee) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Rascal) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoleTypes.Crewmate) && target.Data.IsDead) color = Main.roleColors[CustomRoles.Bait]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoleTypes.Neutral) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Charmed) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Infected) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Contagious) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Egoist) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Recruit) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Necroview) && target.Is(CustomRoles.Soulless) && target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + + // Visionary + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoleTypes.Impostor) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Madmate) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Admired) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Bait]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Parasite) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Crewpostor) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Convict) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Refugee) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Rascal) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Impostor]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoleTypes.Crewmate) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.Bait]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoleTypes.Neutral) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Charmed) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Infected) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Contagious) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Egoist) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Recruit) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + if (seer.Is(CustomRoles.Visionary) && target.Is(CustomRoles.Soulless) && !target.Data.IsDead) color = Main.roleColors[CustomRoles.SwordsMan]; + + + // Rogue + if (seer.Is(CustomRoles.Rogue) && target.Is(CustomRoles.Rogue) && Options.RogueKnowEachOther.GetBool()) color = Main.roleColors[CustomRoles.Rogue]; + + // Jackal recruit + if (seer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Recruit)) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Recruit) && Options.SidekickKnowOtherSidekick.GetBool()) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Recruit) && target.Is(CustomRoles.Jackal)) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Recruit) && target.Is(CustomRoles.Sidekick) && Options.SidekickKnowOtherSidekick.GetBool()) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Recruit) && target.Is(CustomRoles.Recruit) && Options.SidekickKnowOtherSidekick.GetBool()) color = Main.roleColors[CustomRoles.Jackal]; + if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick) && Options.SidekickKnowOtherSidekick.GetBool()) color = Main.roleColors[CustomRoles.Jackal]; + + // Spiritcaller can see Evil Spirits in meetings + if (seer.Is(CustomRoles.Spiritcaller) && target.Is(CustomRoles.EvilSpirit)) color = Main.roleColors[CustomRoles.EvilSpirit]; + + // Monarch seeing knighted players + if (seer.Is(CustomRoles.Monarch) && target.Is(CustomRoles.Knighted)) color = Main.roleColors[CustomRoles.Knighted]; + + // GLOW SQUID IS + // BEST MOB IN MINECRAFT + if (target.Is(CustomRoles.Glow) && Utils.IsActive(SystemTypes.Electrical)) color = Main.roleColors[CustomRoles.Glow]; + + + if (target.Is(CustomRoles.Mare) && Utils.IsActive(SystemTypes.Electrical) && !isMeeting) color = Main.roleColors[CustomRoles.Mare]; + + // Virus + if (seer.Is(CustomRoles.Contagious) && target.Is(CustomRoles.Virus)) color = Main.roleColors[CustomRoles.Virus]; + if (seer.Is(CustomRoles.Virus) && target.Is(CustomRoles.Contagious)) color = Main.roleColors[CustomRoles.Contagious]; + if (seer.Is(CustomRoles.Contagious) && target.Is(CustomRoles.Contagious) && Virus.TargetKnowOtherTarget.GetBool()) color = Main.roleColors[CustomRoles.Virus]; + + if (color != "") return true; + else return seer == target + || (Main.GodMode.Value && seer.AmOwner) + || (Main.PlayerStates[seer.Data.PlayerId].IsDead && seer.Data.IsDead && !seer.IsAlive() && Options.GhostCanSeeOtherRoles.GetBool()) + || (seer.Is(CustomRoles.Mimic) && Main.PlayerStates[target.Data.PlayerId].IsDead && target.Data.IsDead && !target.IsAlive() && Options.MimicCanSeeDeadRoles.GetBool()) + || target.Is(CustomRoles.GM) + || seer.Is(CustomRoles.GM) + || seer.Is(CustomRoles.God) + || (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoleTypes.Impostor)) + || (seer.Is(CustomRoles.Traitor) && target.Is(CustomRoleTypes.Impostor)) + || (seer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick)) + || (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick)) + || (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Jackal)) + || (seer.Is(CustomRoles.Madmate) && target.Is(CustomRoleTypes.Impostor) && Options.MadmateKnowWhosImp.GetBool()) + || (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoles.Madmate) && Options.ImpKnowWhosMadmate.GetBool()) + || (seer.Is(CustomRoles.Madmate) && target.Is(CustomRoles.Madmate) && Options.MadmateKnowWhosMadmate.GetBool()) + || (seer.Is(CustomRoles.Rogue) && target.Is(CustomRoles.Rogue) && Options.RogueKnowEachOther.GetBool()) + || (target.Is(CustomRoles.SuperStar) && Options.EveryOneKnowSuperStar.GetBool()) + || (target.Is(CustomRoles.Workaholic) && Options.WorkaholicVisibleToEveryone.GetBool()) + || (target.Is(CustomRoles.Doctor) && !target.GetCustomRole().IsEvilAddons() && Options.DoctorVisibleToEveryone.GetBool()) + || (target.Is(CustomRoles.Gravestone) && Main.PlayerStates[target.Data.PlayerId].IsDead) + || (target.Is(CustomRoles.Mayor) && Options.MayorRevealWhenDoneTasks.GetBool() && target.GetPlayerTaskState().IsTaskFinished) + || (seer.Is(CustomRoleTypes.Crewmate) && target.Is(CustomRoles.Marshall) && target.GetPlayerTaskState().IsTaskFinished) + // || Mare.KnowTargetRoleColor(target, isMeeting) + || EvilDiviner.IsShowTargetRole(seer, target) + || Ritualist.IsShowTargetRole(seer, target); + } + public static bool TryGetData(PlayerControl seer, PlayerControl target, out string colorCode) + { + colorCode = ""; + var state = Main.PlayerStates[seer.PlayerId]; + if (!state.TargetColorData.TryGetValue(target.PlayerId, out var value)) return false; + colorCode = value; + return true; + } + + public static void Add(byte seerId, byte targetId, string colorCode = "") + { + if (colorCode == "") + { + var target = Utils.GetPlayerById(targetId); + if (target == null) return; + colorCode = target.GetRoleColorCode(); + } + + var state = Main.PlayerStates[seerId]; + if (state.TargetColorData.TryGetValue(targetId, out var value) && colorCode == value) return; + state.TargetColorData.Add(targetId, colorCode); + + SendRPC(seerId, targetId, colorCode); + } + public static void Remove(byte seerId, byte targetId) + { + var state = Main.PlayerStates[seerId]; + if (!state.TargetColorData.ContainsKey(targetId)) return; + state.TargetColorData.Remove(targetId); + + SendRPC(seerId, targetId); + } + public static void RemoveAll(byte seerId) + { + Main.PlayerStates[seerId].TargetColorData.Clear(); + + SendRPC(seerId); + } + private static void SendRPC(byte seerId, byte targetId = byte.MaxValue, string colorCode = "") + { + if (!AmongUsClient.Instance.AmHost) return; + + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetNameColorData, SendOption.Reliable, -1); + writer.Write(seerId); + writer.Write(targetId); + writer.Write(colorCode); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPC(MessageReader reader) + { + byte seerId = reader.ReadByte(); + byte targetId = reader.ReadByte(); + string colorCode = reader.ReadString(); + + if (targetId == byte.MaxValue) + RemoveAll(seerId); + else if (colorCode == "") + Remove(seerId, targetId); + else + Add(seerId, targetId, colorCode); + } +} \ No newline at end of file diff --git a/Modules/NameNotifyManager.cs b/Modules/NameNotifyManager.cs new file mode 100644 index 000000000..aa2fd155c --- /dev/null +++ b/Modules/NameNotifyManager.cs @@ -0,0 +1,64 @@ +using Hazel; +using System.Collections.Generic; + +namespace TOHE; + +public static class NameNotifyManager +{ + private static Dictionary Notice = new(); + public static void Reset() => Notice = new(); + public static bool Notifying(this PlayerControl pc) => Notice.ContainsKey(pc.PlayerId); + public static void Notify(this PlayerControl pc, string text, float time = 4f) + { + if (!AmongUsClient.Instance.AmHost || pc == null) return; + if (!GameStates.IsInTask) return; + if (!text.Contains(" AllValues; + public OptionBackupData(IGameOptions option) + { + AllValues = new(32); + + foreach (ByteOptionNames name in Enum.GetValues(typeof(ByteOptionNames))) + { + if (option.TryGetByte(name, out var value)) + AllValues.Add(new ByteOptionBackupValue(name, value)); + } + foreach (BoolOptionNames name in Enum.GetValues(typeof(BoolOptionNames))) + { + if (option.TryGetBool(name, out var value)) + AllValues.Add(new BoolOptionBackupValue(name, value)); + } + foreach (FloatOptionNames name in Enum.GetValues(typeof(FloatOptionNames))) + { + if (option.TryGetFloat(name, out var value)) + AllValues.Add(new FloatOptionBackupValue(name, value)); + } + foreach (Int32OptionNames name in Enum.GetValues(typeof(Int32OptionNames))) + { + if (option.TryGetInt(name, out var value)) + AllValues.Add(new IntOptionBackupValue(name, value)); + } + // [バニラ側バグ] GetIntで部屋の人数のみ取得できないため、別で取得する + AllValues.Add(new IntOptionBackupValue(Int32OptionNames.MaxPlayers, option.MaxPlayers)); + // TryGetUIntが実装されていないため、別で取得する + AllValues.Add(new UIntOptionBackupValue(UInt32OptionNames.Keywords, (uint)option.Keywords)); + + foreach (RoleTypes role in new RoleTypes[] { RoleTypes.Scientist, RoleTypes.Engineer, RoleTypes.GuardianAngel, RoleTypes.Shapeshifter }) + { + AllValues.Add(new RoleRateBackupValue(role, option.RoleOptions.GetNumPerGame(role), option.RoleOptions.GetChancePerGame(role))); + } + } + + public IGameOptions Restore(IGameOptions option) + { + foreach (var value in AllValues) + { + value.Restore(option); + } + return option; + } + + public byte GetByte(ByteOptionNames name) => Get(name); + public bool GetBool(BoolOptionNames name) => Get(name); + public float GetFloat(FloatOptionNames name) => Get(name); + public int GetInt(Int32OptionNames name) => Get(name); + public uint GetUInt(UInt32OptionNames name) => Get(name); + public TValue Get(TKey name) + where TKey : Enum + { + var value = AllValues + .OfType>() + .Where(val => val.OptionName.Equals(name)). + FirstOrDefault(); + + return value == null ? default : value.Value; + } +} \ No newline at end of file diff --git a/Modules/OptionBackup/OptionBackupValue.cs b/Modules/OptionBackup/OptionBackupValue.cs new file mode 100644 index 000000000..f0a9531cd --- /dev/null +++ b/Modules/OptionBackup/OptionBackupValue.cs @@ -0,0 +1,81 @@ +using AmongUs.GameOptions; +using System; + + +namespace TOHE; + +public abstract class OptionBackupValue +{ + public abstract void Restore(IGameOptions option); +} + +public abstract class OptionBackupValueBase : OptionBackupValue +where NameT : Enum +{ + public readonly NameT OptionName; + public readonly ValueT Value; + public OptionBackupValueBase(NameT name, ValueT value) + { + OptionName = name; + Value = value; + } +} + +public class ByteOptionBackupValue : OptionBackupValueBase +{ + public ByteOptionBackupValue(ByteOptionNames name, byte value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetByte(OptionName, Value); + } +} +public class BoolOptionBackupValue : OptionBackupValueBase +{ + public BoolOptionBackupValue(BoolOptionNames name, bool value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetBool(OptionName, Value); + } +} +public class FloatOptionBackupValue : OptionBackupValueBase +{ + public FloatOptionBackupValue(FloatOptionNames name, float value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetFloat(OptionName, Value); + } +} +public class IntOptionBackupValue : OptionBackupValueBase +{ + public IntOptionBackupValue(Int32OptionNames name, int value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetInt(OptionName, Value); + } +} +public class UIntOptionBackupValue : OptionBackupValueBase +{ + public UIntOptionBackupValue(UInt32OptionNames name, uint value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetUInt(OptionName, Value); + } +} + +public class RoleRateBackupValue : OptionBackupValue +{ + public RoleTypes roleType; + public int maxCount; + public int chance; + + public RoleRateBackupValue(RoleTypes type, int maxCount, int chance) + { + this.roleType = type; + this.maxCount = maxCount; + this.chance = chance; + } + public override void Restore(IGameOptions option) + { + option.RoleOptions.SetRoleRate(roleType, maxCount, chance); + } +} \ No newline at end of file diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs new file mode 100644 index 000000000..c077d0f30 --- /dev/null +++ b/Modules/OptionHolder.cs @@ -0,0 +1,2641 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TOHE.Roles.AddOns.Crewmate; +using TOHE.Roles.AddOns.Impostor; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Impostor; +using TOHE.Roles.Neutral; +using UnityEngine; + +namespace TOHE; + +[Flags] +public enum CustomGameMode +{ + Standard = 0x01, + SoloKombat = 0x02, + All = int.MaxValue +} + +[HarmonyPatch] +public static class Options +{ + static Task taskOptionsLoad; + [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.Initialize)), HarmonyPostfix] + public static void OptionsLoadStart() + { + Logger.Info("Options.Load Start", "Options"); + taskOptionsLoad = Task.Run(Load); + } + [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start)), HarmonyPostfix] + public static void WaitOptionsLoad() + { + taskOptionsLoad.Wait(); + Logger.Info("Options.Load End", "Options"); + } + // オプションId + public const int PresetId = 0; + + // プリセット + private static readonly string[] presets = + { + Main.Preset1.Value, Main.Preset2.Value, Main.Preset3.Value, + Main.Preset4.Value, Main.Preset5.Value + }; + + // ゲームモード + public static OptionItem GameMode; + public static CustomGameMode CurrentGameMode + => GameMode.GetInt() switch + { + 1 => CustomGameMode.SoloKombat, + _ => CustomGameMode.Standard + }; + + public static readonly string[] gameModes = + { + "Standard", "SoloKombat" + }; + + // MapActive + public static bool IsActiveSkeld => AddedTheSkeld.GetBool() || Main.NormalOptions.MapId == 0; + public static bool IsActiveMiraHQ => AddedMiraHQ.GetBool() || Main.NormalOptions.MapId == 1; + public static bool IsActivePolus => AddedPolus.GetBool() || Main.NormalOptions.MapId == 2; + public static bool IsActiveAirship => AddedTheAirship.GetBool() || Main.NormalOptions.MapId == 4; + + // 役職数・確率 + public static Dictionary roleCounts; + public static Dictionary roleSpawnChances; + public static Dictionary CustomRoleCounts; + public static Dictionary CustomRoleSpawnChances; + public static Dictionary CustomAdtRoleSpawnRate; + public static readonly string[] rates = + { + "Rate0", "Rate5", "Rate10", "Rate20", "Rate30", "Rate40", + "Rate50", "Rate60", "Rate70", "Rate80", "Rate90", "Rate100", + }; + public static readonly string[] ratesZeroOne = + { + "RoleOff", /*"Rate10", "Rate20", "Rate30", "Rate40", "Rate50", + "Rate60", "Rate70", "Rate80", "Rate90", */"RoleRate", + }; + public static readonly string[] ratesToggle = + { + "RoleOff", "RoleRate", "RoleOn" + }; + public static readonly string[] CheatResponsesName = + { + "Ban", "Kick", "NoticeMe","NoticeEveryone" + }; + public static readonly string[] ConfirmEjectionsMode = + { + "ConfirmEjections.None", + "ConfirmEjections.Team", + "ConfirmEjections.Role" + }; + public static readonly string[] CamouflageMode = + { + "CamouflageMode.Default", + "CamouflageMode.Host", + "CamouflageMode.Karpe", + "CamouflageMode.Loonie", + "CamouflageMode.Lauryn", + "CamouflageMode.Moe" + }; + + // 各役職の詳細設定 + public static OptionItem EnableGM; + public static float DefaultKillCooldown = Main.NormalOptions?.KillCooldown ?? 20; + public static OptionItem GhostsDoTasks; + + public static OptionItem DisableMeeting; + public static OptionItem DisableCloseDoor; + public static OptionItem DisableSabotage; + public static OptionItem DisableTaskWin; + + public static OptionItem KillFlashDuration; + //public static OptionItem ShareLobby; + //public static OptionItem ShareLobbyMinPlayer; + public static OptionItem DisableVanillaRoles; + public static OptionItem DisableHiddenRoles; + public static OptionItem DisableSunnyboy; + public static OptionItem SunnyboyChance; + public static OptionItem DisableBard; + public static OptionItem BardChance; + public static OptionItem DisableSaboteur; + public static OptionItem CEMode; + public static OptionItem ConfirmEjectionsNK; + public static OptionItem ConfirmEjectionsNonNK; + public static OptionItem ConfirmEjectionsNeutralAsImp; + public static OptionItem ShowImpRemainOnEject; + public static OptionItem ShowNKRemainOnEject; + public static OptionItem ShowTeamNextToRoleNameOnEject; + public static OptionItem CheatResponses; + public static OptionItem LowLoadMode; + + // Dummy Settings + public static OptionItem SpawnSidekickAlone; + + + // Detailed Ejections // + public static OptionItem ExtendedEjections; + public static OptionItem ConfirmEgoistOnEject; + public static OptionItem ConfirmSidekickOnEject; + public static OptionItem ConfirmLoversOnEject; + + + public static OptionItem NonNeutralKillingRolesMinPlayer; + public static OptionItem NonNeutralKillingRolesMaxPlayer; + public static OptionItem NeutralKillingRolesMinPlayer; + public static OptionItem NeutralKillingRolesMaxPlayer; + public static OptionItem CovenRolesMinPlayer; + public static OptionItem CovenRolesMaxPlayer; + public static OptionItem NeutralRoleWinTogether; + public static OptionItem NeutralWinTogether; + + public static OptionItem DefaultShapeshiftCooldown; + public static OptionItem DeadImpCantSabotage; + public static OptionItem ImpKnowAlliesRole; + public static OptionItem ImpKnowWhosMadmate; + public static OptionItem MadmateKnowWhosImp; + public static OptionItem MadmateKnowWhosMadmate; + public static OptionItem MadmateHasImpostorVision; + //public static OptionItem MadmateCanFixSabotage; + public static OptionItem ImpCanKillMadmate; + public static OptionItem MadmateCanKillImp; + public static OptionItem JackalCanKillSidekick; + public static OptionItem SidekickCanKillJackal; + public static OptionItem SidekickKnowOtherSidekick; + public static OptionItem SidekickKnowOtherSidekickRole; + public static OptionItem SidekickCanKillSidekick; + + public static OptionItem ShapeMasterShapeshiftDuration; + public static OptionItem EGCanGuessImp; + public static OptionItem EGCanGuessAdt; + public static OptionItem EGCanGuessTaskDoneSnitch; + public static OptionItem EGCanGuessTime; + public static OptionItem EGTryHideMsg; + public static OptionItem WarlockCanKillAllies; + public static OptionItem WarlockCanKillSelf; + public static OptionItem WarlockShiftDuration; + public static OptionItem ScavengerKillCooldown; + public static OptionItem ZombieKillCooldown; + public static OptionItem ZombieSpeedReduce; + public static OptionItem EvilWatcherChance; + public static OptionItem GGCanGuessCrew; + public static OptionItem GGCanGuessAdt; + public static OptionItem GGCanGuessTime; + public static OptionItem GGTryHideMsg; + public static OptionItem LuckeyProbability; + public static OptionItem LuckyProbability; + public static OptionItem VindicatorAdditionalVote; + public static OptionItem VindicatorHideVote; + public static OptionItem MayorAdditionalVote; + public static OptionItem MayorHasPortableButton; + public static OptionItem MayorNumOfUseButton; + public static OptionItem MayorHideVote; + public static OptionItem MayorRevealWhenDoneTasks; + public static OptionItem OppoImmuneToAttacksWhenTasksDone; + public static OptionItem DoctorTaskCompletedBatteryCharge; + public static OptionItem SpeedBoosterUpSpeed; + public static OptionItem SpeedBoosterTimes; + public static OptionItem GlitchCanVote; + public static OptionItem TrapperBlockMoveTime; + public static OptionItem DetectiveCanknowKiller; + public static OptionItem TransporterTeleportMax; + public static OptionItem CanTerroristSuicideWin; + public static OptionItem InnocentCanWinByImp; + public static OptionItem WorkaholicVentCooldown; + public static OptionItem WorkaholicCannotWinAtDeath; + public static OptionItem WorkaholicVisibleToEveryone; + public static OptionItem WorkaholicGiveAdviceAlive; + public static OptionItem BaitNotification; + public static OptionItem DoctorVisibleToEveryone; + public static OptionItem JackalWinWithSidekick; + public static OptionItem ArsonistDouseTime; + public static OptionItem ArsonistCooldown; + public static OptionItem ArsonistKeepsGameGoing; + public static OptionItem JesterCanUseButton; + public static OptionItem JesterCanVent; + public static OptionItem LegacyMafia; + public static OptionItem NotifyGodAlive; + public static OptionItem MarioVentNumWin; + public static OptionItem MarioVentCD; + public static OptionItem VeteranSkillCooldown; + public static OptionItem VeteranSkillDuration; + public static OptionItem TimeMasterSkillCooldown; + public static OptionItem TimeMasterSkillDuration; + public static OptionItem VeteranSkillMaxOfUseage; + public static OptionItem BodyguardProtectRadius; + public static OptionItem ParanoiaNumOfUseButton; + public static OptionItem ParanoiaVentCooldown; + public static OptionItem ImpKnowCyberStarDead; + public static OptionItem NeutralKnowCyberStarDead; + public static OptionItem EveryOneKnowSuperStar; + public static OptionItem MNKillCooldown; + public static OptionItem MafiaCanKillNum; + public static OptionItem RetributionistCanKillNum; + public static OptionItem MinimumPlayersAliveToRetri; + public static OptionItem CanOnlyRetributeWithTasksDone; + public static OptionItem BomberRadius; + public static OptionItem BomberCanKill; + public static OptionItem BomberKillCD; + public static OptionItem BombCooldown; + public static OptionItem ImpostorsSurviveBombs; + public static OptionItem BomberDiesInExplosion; + public static OptionItem NukerChance; + public static OptionItem NukeRadius; + public static OptionItem NukeCooldown; + + public static OptionItem SkeldChance; + public static OptionItem MiraChance; + public static OptionItem PolusChance; + public static OptionItem AirshipChance; + + // UNDERDOG + public static OptionItem UnderdogKillCooldown; + public static OptionItem UnderdogMaximumPlayersNeededToKill; + + public static OptionItem CleanerKillCooldown; + public static OptionItem KillCooldownAfterCleaning; + public static OptionItem GuardSpellTimes; + public static OptionItem FlashWhenTrapBoobyTrap; + public static OptionItem CapitalismSkillCooldown; + public static OptionItem GrenadierSkillCooldown; + public static OptionItem GrenadierSkillDuration; + public static OptionItem GrenadierCauseVision; + public static OptionItem GrenadierCanAffectNeutral; + public static OptionItem RevolutionistDrawTime; + public static OptionItem RevolutionistCooldown; + public static OptionItem RevolutionistDrawCount; + public static OptionItem RevolutionistKillProbability; + public static OptionItem RevolutionistVentCountDown; + public static OptionItem ShapeImperiusCurseShapeshiftDuration; + public static OptionItem ImperiusCurseShapeshiftCooldown; + public static OptionItem CrewpostorCanKillAllies; + public static OptionItem CrewpostorKnowsAllies; + public static OptionItem AlliesKnowCrewpostor; + public static OptionItem ImpCanBeSeer; + public static OptionItem CrewCanBeSeer; + public static OptionItem NeutralCanBeSeer; + public static OptionItem ImpCanBeAutopsy; + public static OptionItem CrewCanBeAutopsy; + public static OptionItem NeutralCanBeAutopsy; + public static OptionItem ImpCanBeBewilder; + public static OptionItem CrewCanBeBewilder; + public static OptionItem NeutralCanBeBewilder; + public static OptionItem ImpCanBeSunglasses; + public static OptionItem CrewCanBeSunglasses; + public static OptionItem NeutralCanBeSunglasses; + public static OptionItem ImpCanBeGlow; + public static OptionItem CrewCanBeGlow; + public static OptionItem NeutralCanBeGlow; + public static OptionItem ImpCanBeGuesser; + public static OptionItem CrewCanBeGuesser; + public static OptionItem NeutralCanBeGuesser; + public static OptionItem ImpCanBeWatcher; + public static OptionItem CrewCanBeWatcher; + public static OptionItem NeutralCanBeWatcher; + public static OptionItem ImpCanBeNecroview; + public static OptionItem CrewCanBeNecroview; + public static OptionItem NeutralCanBeNecroview; + public static OptionItem ImpCanBeOblivious; + public static OptionItem CrewCanBeOblivious; + public static OptionItem NeutralCanBeOblivious; + public static OptionItem ObliviousBaitImmune; + public static OptionItem ImpCanBeTiebreaker; + public static OptionItem CrewCanBeTiebreaker; + public static OptionItem NeutralCanBeTiebreaker; + public static OptionItem CrewmateCanBeSidekick; + public static OptionItem NeutralCanBeSidekick; + public static OptionItem ImpostorCanBeSidekick; + public static OptionItem ImpCanBeOnbound; + public static OptionItem CrewCanBeOnbound; + public static OptionItem NeutralCanBeOnbound; + public static OptionItem ImpCanBeInLove; + public static OptionItem CrewCanBeInLove; + public static OptionItem NeutralCanBeInLove; + public static OptionItem ImpCanBeReflective; + public static OptionItem CrewCanBeReflective; + public static OptionItem NeutralCanBeReflective; + public static OptionItem ImpCanBeUnreportable; + public static OptionItem CrewCanBeUnreportable; + public static OptionItem NeutralCanBeUnreportable; + public static OptionItem ImpCanBeLucky; + public static OptionItem CrewCanBeLucky; + public static OptionItem NeutralCanBeLucky; + public static OptionItem ImpCanBeUnlucky; + public static OptionItem CrewCanBeUnlucky; + public static OptionItem NeutralCanBeUnlucky; + public static OptionItem UnluckyTaskSuicideChance; + public static OptionItem UnluckyKillSuicideChance; + public static OptionItem UnluckyVentSuicideChance; + public static OptionItem UnluckyReportSuicideChance; + public static OptionItem UnluckySabotageSuicideChance; + // RASCAL // + public static OptionItem RascalAppearAsMadmate; + + + // ROGUE + public static OptionItem ImpCanBeRogue; + public static OptionItem CrewCanBeRogue; + public static OptionItem NeutralCanBeRogue; + public static OptionItem RogueKnowEachOther; + public static OptionItem RogueKnowEachOtherRoles; + + // Gravestone + public static OptionItem ImpCanBeGravestone; + public static OptionItem CrewCanBeGravestone; + public static OptionItem NeutralCanBeGravestone; + + // Mare Add-on + public static OptionItem MareKillCD; + + + + // + public static OptionItem ControlCooldown; + public static OptionItem InhibitorCD; + public static OptionItem SaboteurCD; + public static OptionItem JesterVision; + public static OptionItem PhantomCanVent; + public static OptionItem PhantomSnatchesWin; + // public static OptionItem LawyerVision; + public static OptionItem ImpCanBeDiseased; + public static OptionItem CrewCanBeDiseased; + public static OptionItem NeutralCanBeDiseased; + public static OptionItem DiseasedCDOpt; + public static OptionItem DiseasedCDReset; + + public static OptionItem ImpCanBeAntidote; + public static OptionItem CrewCanBeAntidote; + public static OptionItem NeutralCanBeAntidote; + public static OptionItem AntidoteCDOpt; + public static OptionItem AntidoteCDReset; + + public static OptionItem ImpCanBeBait; + public static OptionItem CrewCanBeBait; + public static OptionItem NeutralCanBeBait; + public static OptionItem BaitDelayMin; + public static OptionItem BaitDelayMax; + public static OptionItem BaitDelayNotify; + public static OptionItem ImpCanBeTrapper; + public static OptionItem CrewCanBeTrapper; + public static OptionItem NeutralCanBeTrapper; + public static OptionItem ImpCanBeFool; + public static OptionItem CrewCanBeFool; + public static OptionItem NeutralCanBeFool; + public static OptionItem LighterVision; + public static OptionItem TasklessCrewCanBeLazy; + public static OptionItem TaskBasedCrewCanBeLazy; + public static OptionItem DovesOfNeaceCooldown; + public static OptionItem DovesOfNeaceMaxOfUseage; + public static OptionItem BTKillCooldown; + public static OptionItem TrapOnlyWorksOnTheBodyBoobyTrap; + public static OptionItem ImpCanBeDoubleShot; + public static OptionItem CrewCanBeDoubleShot; + public static OptionItem NeutralCanBeDoubleShot; + public static OptionItem MimicCanSeeDeadRoles; + + //public static OptionItem NSerialKillerKillCD; + //public static OptionItem NSerialKillerHasImpostorVision; + //public static OptionItem NSerialKillerCanVent; + + public static OptionItem ParasiteCD; + + public static OptionItem ShapeshiftCD; + public static OptionItem ShapeshiftDur; + + public static OptionItem MafiaShapeshiftCD; + public static OptionItem MafiaShapeshiftDur; + + public static OptionItem ScientistDur; + public static OptionItem ScientistCD; + + public static OptionItem GCanGuessImp; + public static OptionItem GCanGuessCrew; + public static OptionItem GCanGuessAdt; + public static OptionItem GCanGuessTaskDoneSnitch; + public static OptionItem GTryHideMsg; + + // Masochist + public static OptionItem MasochistKillMax; + + +//Task Management + public static OptionItem DisableShortTasks; + public static OptionItem DisableCleanVent; + public static OptionItem DisableCalibrateDistributor; + public static OptionItem DisableChartCourse; + public static OptionItem DisableStabilizeSteering; + public static OptionItem DisableCleanO2Filter; + public static OptionItem DisableUnlockManifolds; + public static OptionItem DisablePrimeShields; + public static OptionItem DisableMeasureWeather; + public static OptionItem DisableBuyBeverage; + public static OptionItem DisableAssembleArtifact; + public static OptionItem DisableSortSamples; + public static OptionItem DisableProcessData; + public static OptionItem DisableRunDiagnostics; + public static OptionItem DisableRepairDrill; + public static OptionItem DisableAlignTelescope; + public static OptionItem DisableRecordTemperature; + public static OptionItem DisableFillCanisters; + public static OptionItem DisableMonitorTree; + public static OptionItem DisableStoreArtifacts; + public static OptionItem DisablePutAwayPistols; + public static OptionItem DisablePutAwayRifles; + public static OptionItem DisableMakeBurger; + public static OptionItem DisableCleanToilet; + public static OptionItem DisableDecontaminate; + public static OptionItem DisableSortRecords; + public static OptionItem DisableFixShower; + public static OptionItem DisablePickUpTowels; + public static OptionItem DisablePolishRuby; + public static OptionItem DisableDressMannequin; + public static OptionItem DisableCommonTasks; + public static OptionItem DisableSwipeCard; + public static OptionItem DisableFixWiring; + public static OptionItem DisableEnterIdCode; + public static OptionItem DisableInsertKeys; + public static OptionItem DisableScanBoardingPass; + public static OptionItem DisableLongTasks; + public static OptionItem DisableSubmitScan; + public static OptionItem DisableUnlockSafe; + public static OptionItem DisableStartReactor; + public static OptionItem DisableResetBreaker; + public static OptionItem DisableAlignEngineOutput; + public static OptionItem DisableInspectSample; + public static OptionItem DisableEmptyChute; + public static OptionItem DisableClearAsteroids; + public static OptionItem DisableWaterPlants; + public static OptionItem DisableOpenWaterways; + public static OptionItem DisableReplaceWaterJug; + public static OptionItem DisableRebootWifi; + public static OptionItem DisableDevelopPhotos; + public static OptionItem DisableRewindTapes; + public static OptionItem DisableStartFans; + public static OptionItem DisableOtherTasks; + public static OptionItem DisableUploadData; + public static OptionItem DisableEmptyGarbage; + public static OptionItem DisableFuelEngines; + public static OptionItem DisableDivertPower; + public static OptionItem DisableActivateWeatherNodes; + + // Merchant Filters // + public static OptionItem BaitCanBeSold; + public static OptionItem WatcherCanBeSold; + public static OptionItem SeerCanBeSold; + public static OptionItem TrapperCanBeSold; + public static OptionItem TiebreakerCanBeSold; + public static OptionItem KnightedCanBeSold; + public static OptionItem NecroviewCanBeSold; + public static OptionItem SoullessCanBeSold; + public static OptionItem SchizoCanBeSold; + public static OptionItem OnboundCanBeSold; + public static OptionItem GuesserCanBeSold; + public static OptionItem UnreportableCanBeSold; + public static OptionItem LuckyCanBeSold; + public static OptionItem ObliviousCanBeSold; + public static OptionItem BewilderCanBeSold; + + //デバイスブロック + public static OptionItem DisableDevices; + public static OptionItem DisableSkeldDevices; + public static OptionItem DisableSkeldAdmin; + public static OptionItem DisableSkeldCamera; + public static OptionItem DisableMiraHQDevices; + public static OptionItem DisableMiraHQAdmin; + public static OptionItem DisableMiraHQDoorLog; + public static OptionItem DisablePolusDevices; + public static OptionItem DisablePolusAdmin; + public static OptionItem DisablePolusCamera; + public static OptionItem DisablePolusVital; + public static OptionItem DisableAirshipDevices; + public static OptionItem DisableAirshipCockpitAdmin; + public static OptionItem DisableAirshipRecordsAdmin; + public static OptionItem DisableAirshipCamera; + public static OptionItem DisableAirshipVital; + public static OptionItem DisableDevicesIgnoreConditions; + public static OptionItem DisableDevicesIgnoreImpostors; + public static OptionItem DisableDevicesIgnoreNeutrals; + public static OptionItem DisableDevicesIgnoreCrewmates; + public static OptionItem DisableDevicesIgnoreAfterAnyoneDied; + + // Maps + public static OptionItem RandomMapsMode; + public static OptionItem AddedTheSkeld; + public static OptionItem AddedMiraHQ; + public static OptionItem AddedPolus; + public static OptionItem AddedTheAirship; + public static OptionItem AddedDleks; + public static OptionItem RandomSpawn; + public static OptionItem AirshipAdditionalSpawn; + public static OptionItem AirshipVariableElectrical; + public static OptionItem DisableAirshipMovingPlatform; + + // Sabotage + public static OptionItem CommsCamouflage; + public static OptionItem DisableReportWhenCC; + public static OptionItem SabotageTimeControl; + public static OptionItem PolusReactorTimeLimit; + public static OptionItem AirshipReactorTimeLimit; + public static OptionItem LightsOutSpecialSettings; + public static OptionItem DisableAirshipViewingDeckLightsPanel; + public static OptionItem DisableAirshipGapRoomLightsPanel; + public static OptionItem DisableAirshipCargoLightsPanel; + + //Guesser Mode// + public static OptionItem GuesserMode; + public static OptionItem CrewmatesCanGuess; + public static OptionItem ImpostorsCanGuess; + public static OptionItem NeutralKillersCanGuess; + public static OptionItem PassiveNeutralsCanGuess; + public static OptionItem HideGuesserCommands; + public static OptionItem CanGuessAddons; + public static OptionItem ImpCanGuessImp; + public static OptionItem CrewCanGuessCrew; + + // Guesser Mode - Addon Config // + public static OptionItem AddonSettingsCrew; + public static OptionItem ClaimAddonSettingsCrew; + public static OptionItem BetrayalAddonSettingsCrew; + public static OptionItem ImpOnlyAddonSettingsCrew; + public static OptionItem CrewOnlyAddonSettingsCrew; + public static OptionItem NeutralAddonSettingsCrew; + public static OptionItem BasicAddonSettingsCrew; + + public static OptionItem AddonSettingsImp; + public static OptionItem ClaimAddonSettingsImp; + public static OptionItem BetrayalAddonSettingsImp; + public static OptionItem ImpOnlyAddonSettingsImp; + public static OptionItem CrewOnlyAddonSettingsImp; + public static OptionItem NeutralAddonSettingsImp; + public static OptionItem BasicAddonSettingsImp; + public static OptionItem AddonSettingsNeut; + public static OptionItem ClaimAddonSettingsNeut; + public static OptionItem BetrayalAddonSettingsNeut; + public static OptionItem ImpOnlyAddonSettingsNeut; + public static OptionItem CrewOnlyAddonSettingsNeut; + public static OptionItem NeutralAddonSettingsNeut; + public static OptionItem BasicAddonSettingsNeut; + + + // 投票モード + public static OptionItem VoteMode; + public static OptionItem WhenSkipVote; + public static OptionItem WhenSkipVoteIgnoreFirstMeeting; + public static OptionItem WhenSkipVoteIgnoreNoDeadBody; + public static OptionItem WhenSkipVoteIgnoreEmergency; + public static OptionItem WhenNonVote; + public static OptionItem WhenTie; + public static readonly string[] voteModes = + { + "Default", "Suicide", "SelfVote", "Skip" + }; + public static readonly string[] tieModes = + { + "TieMode.Default", "TieMode.All", "TieMode.Random" + }; + /* public static readonly string[] addonGuessModeCrew = + { + "GuesserMode.All", "GuesserMode.Harmful", "GuesserMode.Random" + }; */ + public static readonly string[] madmateSpawnMode = + { + "MadmateSpawnMode.Assign", + "MadmateSpawnMode.FirstKill", + "MadmateSpawnMode.SelfVote", + }; + public static readonly string[] madmateCountMode = + { + "MadmateCountMode.None", + "MadmateCountMode.Imp", + "MadmateCountMode.Crew", + }; + public static readonly string[] sidekickCountMode = + { + "SidekickCountMode.Jackal", + "SidekickCountMode.None", + "SidekickCountMode.Original", + }; + public static VoteMode GetWhenSkipVote() => (VoteMode)WhenSkipVote.GetValue(); + public static VoteMode GetWhenNonVote() => (VoteMode)WhenNonVote.GetValue(); + + // ボタン回数 + public static OptionItem SyncButtonMode; + public static OptionItem SyncedButtonCount; + public static int UsedButtonCount = 0; + + // 全員生存時の会議時間 + public static OptionItem AllAliveMeeting; + public static OptionItem AllAliveMeetingTime; + + // 追加の緊急ボタンクールダウン + public static OptionItem AdditionalEmergencyCooldown; + public static OptionItem AdditionalEmergencyCooldownThreshold; + public static OptionItem AdditionalEmergencyCooldownTime; + + //転落死 + public static OptionItem LadderDeath; + public static OptionItem LadderDeathChance; + + // タスク上書き + public static OverrideTasksData TerroristTasks; + public static OverrideTasksData TransporterTasks; + public static OverrideTasksData WorkaholicTasks; + public static OverrideTasksData CrewpostorTasks; + public static OverrideTasksData PhantomTasks; + public static OverrideTasksData GuardianTasks; + public static OverrideTasksData OpportunistTasks; + public static OverrideTasksData MayorTasks; + public static OverrideTasksData RetributionistTasks; + public static OverrideTasksData TimeManagerTasks; + + // その他 + public static OptionItem FixFirstKillCooldown; + public static OptionItem ShieldPersonDiedFirst; + public static OptionItem GhostCanSeeOtherRoles; + public static OptionItem GhostCanSeeOtherVotes; + public static OptionItem GhostCanSeeDeathReason; + public static OptionItem GhostIgnoreTasks; + public static OptionItem KPDCamouflageMode; + + // Guess Restrictions // + public static OptionItem TerroristCanGuess; + public static OptionItem WorkaholicCanGuess; + public static OptionItem PhantomCanGuess; + public static OptionItem GodCanGuess; + + // プリセット対象外 + public static OptionItem AllowConsole; + public static OptionItem NoGameEnd; + public static OptionItem AutoDisplayLastRoles; + public static OptionItem AutoDisplayKillLog; + public static OptionItem AutoDisplayLastResult; + public static OptionItem SuffixMode; + public static OptionItem HideGameSettings; + public static OptionItem FormatNameMode; + public static OptionItem ColorNameMode; + public static OptionItem DisableEmojiName; + public static OptionItem ChangeNameToRoleInfo; + public static OptionItem SendRoleDescriptionFirstMeeting; + public static OptionItem RoleAssigningAlgorithm; + public static OptionItem EndWhenPlayerBug; + + public static OptionItem EnableUpMode; + public static OptionItem AutoKickStart; + public static OptionItem AutoKickStartAsBan; + public static OptionItem AutoKickStartTimes; + public static OptionItem AutoKickStopWords; + public static OptionItem AutoKickStopWordsAsBan; + public static OptionItem AutoKickStopWordsTimes; + public static OptionItem KickAndroidPlayer; + public static OptionItem KickXboxPlayer; + public static OptionItem KickPlayStationPlayer; + public static OptionItem KickNintendoPlayer; + public static OptionItem ApplyDenyNameList; + public static OptionItem KickPlayerFriendCodeNotExist; + public static OptionItem KickLowLevelPlayer; + public static OptionItem ApplyBanList; + public static OptionItem ApplyModeratorList; + public static OptionItem ApplyAllowList; + public static OptionItem AutoWarnStopWords; + + public static OptionItem AllowSayCommand; + public static OptionItem ApplyReminderMsg; + public static OptionItem TimeForReminder; + + public static OptionItem DIYGameSettings; + public static OptionItem PlayerCanSetColor; + + //Add-Ons + public static OptionItem NameDisplayAddons; + public static OptionItem AddBracketsToAddons; + public static OptionItem NoLimitAddonsNumMax; + public static OptionItem BewilderVision; + public static OptionItem JesterHasImpostorVision; + public static OptionItem SunglassesVision; + public static OptionItem ImpCanBeAvanger; + public static OptionItem MadmateSpawnMode; + public static OptionItem MadmateCountMode; + public static OptionItem SheriffCanBeMadmate; + public static OptionItem MayorCanBeMadmate; + public static OptionItem NGuesserCanBeMadmate; + public static OptionItem SnitchCanBeMadmate; + public static OptionItem JudgeCanBeMadmate; + public static OptionItem MarshallCanBeMadmate; + public static OptionItem RetributionistCanBeMadmate; + public static OptionItem FarseerCanBeMadmate; + public static OptionItem MadSnitchTasks; + public static OptionItem FlashmanSpeed; + public static OptionItem ButtonBarryButtons; + public static OptionItem LoverSpawnChances; + public static OptionItem LoverKnowRoles; + public static OptionItem LoverSuicide; + public static OptionItem ImpCanBeEgoist; + public static OptionItem ImpEgoistVisibalToAllies; + public static OptionItem CrewCanBeEgoist; + public static OptionItem TicketsPerKill; + public static OptionItem ImpCanBeDualPersonality; + public static OptionItem CrewCanBeDualPersonality; + public static OptionItem DualVotes; + public static OptionItem HideDualVotes; + public static OptionItem ImpCanBeLoyal; + public static OptionItem CrewCanBeLoyal; + public static OptionItem MinWaitAutoStart; + public static OptionItem MaxWaitAutoStart; + public static OptionItem PlayerAutoStart; + //public static OptionItem SidekickCountMode; + + public static readonly string[] suffixModes = + { + "SuffixMode.None", + "SuffixMode.Version", + "SuffixMode.Streaming", + "SuffixMode.Recording", + "SuffixMode.RoomHost", + "SuffixMode.OriginalName", + "SuffixMode.DoNotKillMe", + "SuffixMode.NoAndroidPlz", + "SuffixMode.AutoHost" }; + public static readonly string[] roleAssigningAlgorithms = + { + "RoleAssigningAlgorithm.Default", + "RoleAssigningAlgorithm.NetRandom", + "RoleAssigningAlgorithm.HashRandom", + "RoleAssigningAlgorithm.Xorshift", + "RoleAssigningAlgorithm.MersenneTwister", + }; + public static readonly string[] formatNameModes = + { + "FormatNameModes.None", + "FormatNameModes.Color", + "FormatNameModes.Snacks", + }; + public static SuffixModes GetSuffixMode() => (SuffixModes)SuffixMode.GetValue(); + + public static int SnitchExposeTaskLeft = 1; + + public static bool IsLoaded = false; + + static Options() + { + ResetRoleCounts(); + } + public static void ResetRoleCounts() + { + roleCounts = new Dictionary(); + roleSpawnChances = new Dictionary(); + + foreach (var role in Enum.GetValues(typeof(CustomRoles)).Cast()) + { + roleCounts.Add(role, 0); + roleSpawnChances.Add(role, 0); + } + } + + public static void SetRoleCount(CustomRoles role, int count) + { + roleCounts[role] = count; + + if (CustomRoleCounts.TryGetValue(role, out var option)) + { + option.SetValue(count - 1); + } + } + + public static int GetRoleSpawnMode(CustomRoles role) + { + var mode = CustomRoleSpawnChances.TryGetValue(role, out var sc) ? sc.GetChance() : 0; + return mode switch + { + 0 => 0, + 1 => 1, + 2 => 2, + 100 => 1, + _ => 1, + }; + } + public static int GetRoleCount(CustomRoles role) + { + var mode = GetRoleSpawnMode(role); + return mode is 0 ? 0 : CustomRoleCounts.TryGetValue(role, out var option) ? option.GetInt() : roleCounts[role]; + } + public static float GetRoleChance(CustomRoles role) + { + return CustomRoleSpawnChances.TryGetValue(role, out var option) ? option.GetValue()/* / 10f */ : roleSpawnChances[role]; + } + public static void Load() + { + if (IsLoaded) return; + // 预设 + _ = PresetOptionItem.Create(0, TabGroup.SystemSettings) + .SetColor(new Color32(255, 235, 4, byte.MaxValue)) + .SetHeader(true); + + // 游戏模式 + GameMode = StringOptionItem.Create(1, "GameMode", gameModes, 0, TabGroup.GameSettings, false) + .SetHeader(true); + + #region 职业详细设置 + CustomRoleCounts = new(); + CustomRoleSpawnChances = new(); + CustomAdtRoleSpawnRate = new(); + + // GM + EnableGM = BooleanOptionItem.Create(100, "GM", false, TabGroup.GameSettings, false) + .SetColor(Utils.GetRoleColor(CustomRoles.GM)) + .SetHeader(true); + + // 各职业的总体设定 + ImpKnowAlliesRole = BooleanOptionItem.Create(150, "ImpKnowAlliesRole", true, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true); + ImpKnowWhosMadmate = BooleanOptionItem.Create(151, "ImpKnowWhosMadmate", false, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard); + ImpCanKillMadmate = BooleanOptionItem.Create(152, "ImpCanKillMadmate", true, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard); + + MadmateKnowWhosMadmate = BooleanOptionItem.Create(153, "MadmateKnowWhosMadmate", false, TabGroup.ImpostorRoles, false) + .SetHeader(true) + .SetGameMode(CustomGameMode.Standard); + MadmateKnowWhosImp = BooleanOptionItem.Create(154, "MadmateKnowWhosImp", true, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard); + MadmateCanKillImp = BooleanOptionItem.Create(155, "MadmateCanKillImp", true, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard); + MadmateHasImpostorVision = BooleanOptionItem.Create(156, "MadmateHasImpostorVision", true, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard); + //MadmateCanFixSabotage = BooleanOptionItem.Create(157, "MadmateCanFixSabotage", false, TabGroup.ImpostorRoles, false) + //.SetGameMode(CustomGameMode.Standard); + + DefaultShapeshiftCooldown = FloatOptionItem.Create(200, "DefaultShapeshiftCooldown", new(5f, 999f, 5f), 15f, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetValueFormat(OptionFormat.Seconds); + DeadImpCantSabotage = BooleanOptionItem.Create(201, "DeadImpCantSabotage", false, TabGroup.ImpostorRoles, false) + .SetGameMode(CustomGameMode.Standard); + + NonNeutralKillingRolesMinPlayer = IntegerOptionItem.Create(202, "NonNeutralKillingRolesMinPlayer", new(0, 15, 1), 0, TabGroup.NeutralRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetValueFormat(OptionFormat.Players); + NonNeutralKillingRolesMaxPlayer = IntegerOptionItem.Create(203, "NonNeutralKillingRolesMaxPlayer", new(0, 15, 1), 0, TabGroup.NeutralRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetValueFormat(OptionFormat.Players); + + NeutralKillingRolesMinPlayer = IntegerOptionItem.Create(204, "NeutralKillingRolesMinPlayer", new(0, 15, 1), 0, TabGroup.NeutralRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetValueFormat(OptionFormat.Players); + NeutralKillingRolesMaxPlayer = IntegerOptionItem.Create(205, "NeutralKillingRolesMaxPlayer", new(0, 15, 1), 0, TabGroup.NeutralRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetValueFormat(OptionFormat.Players); + + // CovenRolesMinPlayer = IntegerOptionItem.Create(206, "CovenRolesMinPlayer", new(0, 15, 1), 0, TabGroup.NeutralRoles, false) + // .SetGameMode(CustomGameMode.Standard) + // .SetHeader(true) + // .SetValueFormat(OptionFormat.Players); + // CovenRolesMaxPlayer = IntegerOptionItem.Create(207, "CovenRolesMaxPlayer", new(0, 15, 1), 0, TabGroup.NeutralRoles, false) + // .SetGameMode(CustomGameMode.Standard) + // .SetValueFormat(OptionFormat.Players); + + NeutralRoleWinTogether = BooleanOptionItem.Create(208, "NeutralRoleWinTogether", false, TabGroup.NeutralRoles, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true); + NeutralWinTogether = BooleanOptionItem.Create(209, "NeutralWinTogether", false, TabGroup.NeutralRoles, false) + .SetParent(NeutralRoleWinTogether) + .SetGameMode(CustomGameMode.Standard); + + NameDisplayAddons = BooleanOptionItem.Create(210, "NameDisplayAddons", true, TabGroup.Addons, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true); + NoLimitAddonsNumMax = IntegerOptionItem.Create(211, "NoLimitAddonsNumMax", new(1, 15, 1), 1, TabGroup.Addons, false); + +//==================================================================================================================================// + + // Impostor + TextOptionItem.Create(100000, "RoleType.VanillaRoles", TabGroup.ImpostorRoles) // Vanilla + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(255, 25, 25, byte.MaxValue)); + + SetupRoleOptions(300, TabGroup.ImpostorRoles, CustomRoles.ImpostorTOHE); + SetupRoleOptions(400, TabGroup.ImpostorRoles, CustomRoles.ShapeshifterTOHE); + ShapeshiftCD = FloatOptionItem.Create(402, "ShapeshiftCooldown", new(1f, 999f, 1f), 15f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ShapeshifterTOHE]) + .SetValueFormat(OptionFormat.Seconds); + ShapeshiftDur = FloatOptionItem.Create(403, "ShapeshiftDuration", new(1f, 999f, 1f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ShapeshifterTOHE]) + .SetValueFormat(OptionFormat.Seconds); + + TextOptionItem.Create(100001, "RoleType.ImpKilling", TabGroup.ImpostorRoles) // KILLING + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(255, 25, 25, byte.MaxValue));// KILLING + + EvilTracker.SetupCustomOption(); + Sans.SetupCustomOption();//Arrogance + SetupRoleOptions(1200, TabGroup.ImpostorRoles, CustomRoles.EvilGuesser); + EGCanGuessTime = IntegerOptionItem.Create(1205, "GuesserCanGuessTimes", new(1, 15, 1), 15, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.EvilGuesser]) + .SetValueFormat(OptionFormat.Times); + EGCanGuessImp = BooleanOptionItem.Create(1206, "EGCanGuessImp", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.EvilGuesser]); + EGCanGuessAdt = BooleanOptionItem.Create(1207, "EGCanGuessAdt", false, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.EvilGuesser]); + EGCanGuessTaskDoneSnitch = BooleanOptionItem.Create(1208, "EGCanGuessTaskDoneSnitch", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.EvilGuesser]); + EGTryHideMsg = BooleanOptionItem.Create(1209, "GuesserTryHideMsg", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.EvilGuesser]) + .SetColor(Color.green); + SetupRoleOptions(2400, TabGroup.ImpostorRoles, CustomRoles.Bomber); + BomberRadius = FloatOptionItem.Create(2018, "BomberRadius", new(0.5f, 5f, 0.5f), 2f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bomber]) + .SetValueFormat(OptionFormat.Multiplier); + BomberCanKill = BooleanOptionItem.Create(2015, "CanKill", false, TabGroup.ImpostorRoles, false) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Bomber]); + BomberKillCD = FloatOptionItem.Create(2020, "KillCooldown", new(0f, 999f, 2.5f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(BomberCanKill) + .SetValueFormat(OptionFormat.Seconds); + BombCooldown = FloatOptionItem.Create(2030, "BombCooldown", new(5f, 999f, 2.5f), 60f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bomber]) + .SetValueFormat(OptionFormat.Seconds); + ImpostorsSurviveBombs = BooleanOptionItem.Create(2031, "ImpostorsSurviveBombs", true, TabGroup.ImpostorRoles, false) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Bomber]); + BomberDiesInExplosion = BooleanOptionItem.Create(2032, "BomberDiesInExplosion", true, TabGroup.ImpostorRoles, false) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Bomber]); + NukerChance = IntegerOptionItem.Create(2033, "NukerChance", new(0, 100, 5), 0, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bomber]) + .SetValueFormat(OptionFormat.Percent); + NukeCooldown = FloatOptionItem.Create(2035, "NukeCooldown", new(5f, 999f, 2.5f), 60f, TabGroup.ImpostorRoles, false) + .SetParent(NukerChance) + .SetValueFormat(OptionFormat.Seconds); + NukeRadius = FloatOptionItem.Create(2034, "NukeRadius", new(10f, 100f, 1f), 25f, TabGroup.ImpostorRoles, false) + .SetParent(NukerChance) + .SetValueFormat(OptionFormat.Multiplier); + BountyHunter.SetupCustomOption(); + Councillor.SetupCustomOption(); + SetupRoleOptions(1000, TabGroup.ImpostorRoles, CustomRoles.CursedWolf); //TOH_Y + GuardSpellTimes = IntegerOptionItem.Create(1010, "GuardSpellTimes", new(1, 15, 1), 3, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.CursedWolf]) + .SetValueFormat(OptionFormat.Times); + Deathpact.SetupCustomOption(); + Greedier.SetupCustomOption(); //TOH_Y + Hangman.SetupCustomOption(); + SetupRoleOptions(1500, TabGroup.ImpostorRoles, CustomRoles.Inhibitor); + InhibitorCD = FloatOptionItem.Create(1510, "KillCooldown", new(0f, 999f, 2.5f), 20f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Inhibitor]) + .SetValueFormat(OptionFormat.Seconds); + Lurker.SetupCustomOption(); + // Mare.SetupCustomOption(); + SerialKiller.SetupCustomOption(); + Assassin.SetupCustomOption(); + QuickShooter.SetupCustomOption(); + SetupRoleOptions(10005, TabGroup.ImpostorRoles, CustomRoles.Saboteur); + SaboteurCD = FloatOptionItem.Create(10015, "KillCooldown", new(0f, 999f, 2.5f), 20f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Saboteur]) + .SetValueFormat(OptionFormat.Seconds); + + Sniper.SetupCustomOption(); + Witch.SetupCustomOption(); //spellcaster + SetupRoleOptions(10025, TabGroup.ImpostorRoles, CustomRoles.Underdog); + UnderdogMaximumPlayersNeededToKill = IntegerOptionItem.Create(10030, "UnderdogMaximumPlayersNeededToKill", new(1, 15, 1), 5, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Underdog]) + .SetValueFormat(OptionFormat.Players); + UnderdogKillCooldown = FloatOptionItem.Create(10031, "KillCooldown", new(0f, 999f, 2.5f), 12.5f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Underdog]) + .SetValueFormat(OptionFormat.Seconds); + + + TextOptionItem.Create(100002, "RoleType.ImpSupport", TabGroup.ImpostorRoles)// SUPPORT + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 25, 25, byte.MaxValue));// SUPPORT + Camouflager.SetupCustomOption(); + SetupRoleOptions(2600, TabGroup.ImpostorRoles, CustomRoles.Cleaner); + CleanerKillCooldown = FloatOptionItem.Create(2610, "KillCooldown", new(5f, 999f, 2.5f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Cleaner]) + .SetValueFormat(OptionFormat.Seconds); + KillCooldownAfterCleaning = FloatOptionItem.Create(2611, "KillCooldownAfterCleaning", new(5f, 999f, 2.5f), 60f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Cleaner]) + .SetValueFormat(OptionFormat.Seconds); + EvilDiviner.SetupCustomOption(); + AntiAdminer.SetupCustomOption(); + FireWorks.SetupCustomOption(); + Gangster.SetupCustomOption(); + SetupRoleOptions(555420, TabGroup.ImpostorRoles, CustomRoles.Godfather); + Morphling.SetupCustomOption(); + SetupRoleOptions(3100, TabGroup.ImpostorRoles, CustomRoles.Mafia); + MafiaCanKillNum = IntegerOptionItem.Create(3200, "MafiaCanKillNum", new(0, 15, 1), 1, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mafia]) + .SetValueFormat(OptionFormat.Players); + LegacyMafia = BooleanOptionItem.Create(3210, "LegacyMafia", false, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mafia]); + MafiaShapeshiftCD = FloatOptionItem.Create(3211, "ShapeshiftCooldown", new(1f, 999f, 1f), 15f, TabGroup.ImpostorRoles, false) + .SetParent(LegacyMafia) + .SetValueFormat(OptionFormat.Seconds); + MafiaShapeshiftDur = FloatOptionItem.Create(3212, "ShapeshiftDuration", new(1f, 999f, 1f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(LegacyMafia) + .SetValueFormat(OptionFormat.Seconds); + TimeThief.SetupCustomOption(); + SetupRoleOptions(3400, TabGroup.ImpostorRoles, CustomRoles.Vindicator); + VindicatorAdditionalVote = IntegerOptionItem.Create(3410, "MayorAdditionalVote", new(1, 99, 1), 3, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Vindicator]) + .SetValueFormat(OptionFormat.Votes); + VindicatorHideVote = BooleanOptionItem.Create(3411, "MayorHideVote", false, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Vindicator]); + SetupRoleOptions(16150, TabGroup.ImpostorRoles, CustomRoles.Visionary); + + + TextOptionItem.Create(100003, "RoleType.ImpConcealing", TabGroup.ImpostorRoles) //CONCEALING + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 25, 25, byte.MaxValue));//CONCEALING + SetupRoleOptions(3600, TabGroup.ImpostorRoles, CustomRoles.Escapee); + SetupRoleOptions(3700, TabGroup.ImpostorRoles, CustomRoles.ImperiusCurse); + ShapeImperiusCurseShapeshiftDuration = FloatOptionItem.Create(3710, "ShapeshiftDuration", new(2.5f, 999f, 2.5f), 300, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ImperiusCurse]) + .SetValueFormat(OptionFormat.Seconds); + ImperiusCurseShapeshiftCooldown = FloatOptionItem.Create(3711, "ShapeshiftCooldown", new(1f, 999f, 1f), 15f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ImperiusCurse]) + .SetValueFormat(OptionFormat.Seconds); + SetupRoleOptions(3800, TabGroup.ImpostorRoles, CustomRoles.Miner); + SetupRoleOptions(3900, TabGroup.ImpostorRoles, CustomRoles.Puppeteer); + SetupRoleOptions(4000, TabGroup.ImpostorRoles, CustomRoles.Scavenger); + ScavengerKillCooldown = FloatOptionItem.Create(4010, "KillCooldown", new(5f, 999f, 2.5f), 40f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Scavenger]) + .SetValueFormat(OptionFormat.Seconds); + SetupRoleOptions(4100, TabGroup.ImpostorRoles, CustomRoles.ShapeMaster); + ShapeMasterShapeshiftDuration = FloatOptionItem.Create(4110, "ShapeshiftDuration", new(1, 999, 1), 10, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ShapeMaster]) + .SetValueFormat(OptionFormat.Seconds); + Swooper.SetupCustomOption(); + SetupRoleOptions(4300, TabGroup.ImpostorRoles, CustomRoles.Trickster); + Vampire.SetupCustomOption(); + SetupRoleOptions(4600, TabGroup.ImpostorRoles, CustomRoles.Warlock); + WarlockCanKillAllies = BooleanOptionItem.Create(4610, "CanKillAllies", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Warlock]); + WarlockCanKillSelf = BooleanOptionItem.Create(4611, "CanKillSelf", false, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Warlock]); + WarlockShiftDuration = FloatOptionItem.Create(4612, "ShapeshiftDuration", new(1, 999, 1), 1, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Warlock]) + .SetValueFormat(OptionFormat.Seconds); + Wildling.SetupCustomOption(); + + TextOptionItem.Create(100069, "RoleType.ImpHindering", TabGroup.ImpostorRoles) //HINDERING + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 25, 25, byte.MaxValue));//HINDERING + Hacker.SetupCustomOption(); //anonymous + Dazzler.SetupCustomOption(); + Devourer.SetupCustomOption(); + Twister.SetupCustomOption(); + + + TextOptionItem.Create(100004, "RoleType.Madmate", TabGroup.ImpostorRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 25, 25, byte.MaxValue)); + SetupRoleOptions(4800, TabGroup.ImpostorRoles, CustomRoles.Crewpostor); + CrewpostorCanKillAllies = BooleanOptionItem.Create(4810, "CanKillAllies", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Crewpostor]); + CrewpostorKnowsAllies = BooleanOptionItem.Create(4811, "CrewpostorKnowsAllies", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Crewpostor]); + AlliesKnowCrewpostor = BooleanOptionItem.Create(4812, "AlliesKnowCrewpostor", true, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Crewpostor]); + CrewpostorTasks = OverrideTasksData.Create(4813, TabGroup.ImpostorRoles, CustomRoles.Crewpostor); + SetupSingleRoleOptions(4900, TabGroup.ImpostorRoles, CustomRoles.Parasite, 1, zeroOne: false); + ParasiteCD = FloatOptionItem.Create(4910, "KillCooldown", new(0f, 999f, 2.5f), 30f, TabGroup.ImpostorRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Parasite]) + .SetValueFormat(OptionFormat.Seconds); +//==================================================================================================================================// + + // Crewmate + TextOptionItem.Create(100005, "RoleType.VanillaRoles", TabGroup.CrewmateRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(140, 255, 255, byte.MaxValue)); + SetupRoleOptions(5050, TabGroup.CrewmateRoles, CustomRoles.CrewmateTOHE); + SetupRoleOptions(5000, TabGroup.CrewmateRoles, CustomRoles.EngineerTOHE); + SetupRoleOptions(5100, TabGroup.CrewmateRoles, CustomRoles.ScientistTOHE); + ScientistCD = FloatOptionItem.Create(5110, "VitalsCooldown", new(1f, 250f, 1f), 3f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ScientistTOHE]) + .SetValueFormat(OptionFormat.Seconds); + ScientistDur = FloatOptionItem.Create(5111, "VitalsDuration", new(1f, 250f, 1f), 15f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.ScientistTOHE]) + .SetValueFormat(OptionFormat.Seconds); + + TextOptionItem.Create(100006, "RoleType.CrewBasic", TabGroup.CrewmateRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(140, 255, 255, byte.MaxValue)); + Addict.SetupCustomOption(); + + SetupRoleOptions(5300, TabGroup.CrewmateRoles, CustomRoles.CyberStar); + ImpKnowCyberStarDead = BooleanOptionItem.Create(5400, "ImpKnowCyberStarDead", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.CyberStar]); + NeutralKnowCyberStarDead = BooleanOptionItem.Create(5500, "NeutralKnowCyberStarDead", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.CyberStar]); + SetupRoleOptions(5600, TabGroup.CrewmateRoles, CustomRoles.Doctor); + DoctorTaskCompletedBatteryCharge = FloatOptionItem.Create(5610, "DoctorTaskCompletedBatteryCharge", new(0f, 250f, 1f), 50f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Doctor]) + .SetValueFormat(OptionFormat.Seconds); + DoctorVisibleToEveryone = BooleanOptionItem.Create(5611, "DoctorVisibleToEveryone", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Doctor]); + SetupRoleOptions(5700, TabGroup.CrewmateRoles, CustomRoles.Needy); + SetupRoleOptions(5800, TabGroup.CrewmateRoles, CustomRoles.Luckey); + LuckeyProbability = IntegerOptionItem.Create(5900, "LuckeyProbability", new(0, 100, 5), 50, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Luckey]) + .SetValueFormat(OptionFormat.Percent); + SetupRoleOptions(6000, TabGroup.CrewmateRoles, CustomRoles.SuperStar); + EveryOneKnowSuperStar = BooleanOptionItem.Create(6010, "EveryOneKnowSuperStar", true, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.SuperStar]); + Tracefinder.SetupCustomOption(); + SetupRoleOptions(6200, TabGroup.CrewmateRoles, CustomRoles.Transporter); + TransporterTeleportMax = IntegerOptionItem.Create(6210, "TransporterTeleportMax", new(1, 99, 1), 5, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Transporter]) + .SetValueFormat(OptionFormat.Times); + TransporterTasks = OverrideTasksData.Create(6211, TabGroup.CrewmateRoles, CustomRoles.Transporter); + + TextOptionItem.Create(100007, "RoleType.CrewSupport", TabGroup.CrewmateRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(140, 255, 255, byte.MaxValue)); + Admirer.SetupCustomOption(); + Chameleon.SetupCustomOption(); + Bloodhound.SetupCustomOption(); + Deputy.SetupCustomOption(); + SetupRoleOptions(6600, TabGroup.CrewmateRoles, CustomRoles.Detective); + DetectiveCanknowKiller = BooleanOptionItem.Create(6610, "DetectiveCanknowKiller", true, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Detective]); + Divinator.SetupCustomOption(); // Fortune Teller + SetupRoleOptions(6800, TabGroup.CrewmateRoles, CustomRoles.Grenadier); + GrenadierSkillCooldown = FloatOptionItem.Create(6810, "GrenadierSkillCooldown", new(1f, 180f, 1f), 25f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Grenadier]) + .SetValueFormat(OptionFormat.Seconds); + GrenadierSkillDuration = FloatOptionItem.Create(6811, "GrenadierSkillDuration", new(1f, 999f, 1f), 10f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Grenadier]) + .SetValueFormat(OptionFormat.Seconds); + GrenadierCauseVision = FloatOptionItem.Create(6812, "GrenadierCauseVision", new(0f, 5f, 0.05f), 0.3f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Grenadier]) + .SetValueFormat(OptionFormat.Multiplier); + GrenadierCanAffectNeutral = BooleanOptionItem.Create(6813, "GrenadierCanAffectNeutral", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Grenadier]); + ParityCop.SetupCustomOption(); + SabotageMaster.SetupCustomOption(); //Mechanic + Medic.SetupCustomOption(); + Mediumshiper.SetupCustomOption(); + Merchant.SetupCustomOption(); + Mortician.SetupCustomOption(); + SetupRoleOptions(7500, TabGroup.CrewmateRoles, CustomRoles.Observer); + Oracle.SetupCustomOption(); + SetupRoleOptions(7700, TabGroup.CrewmateRoles, CustomRoles.DovesOfNeace); + DovesOfNeaceCooldown = FloatOptionItem.Create(7710, "DovesOfNeaceCooldown", new(1f, 180f, 1f), 30f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DovesOfNeace]) + .SetValueFormat(OptionFormat.Seconds); + DovesOfNeaceMaxOfUseage = IntegerOptionItem.Create(7711, "DovesOfNeaceMaxOfUseage", new(1, 999, 1), 3, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DovesOfNeace]) + .SetValueFormat(OptionFormat.Times); + SetupRoleOptions(7800, TabGroup.CrewmateRoles, CustomRoles.Paranoia); + ParanoiaNumOfUseButton = IntegerOptionItem.Create(7810, "ParanoiaNumOfUseButton", new(1, 99, 1), 3, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Paranoia]) + .SetValueFormat(OptionFormat.Times); + ParanoiaVentCooldown = FloatOptionItem.Create(7811, "ParanoiaVentCooldown", new(0, 990, 1), 10, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Paranoia]) + .SetValueFormat(OptionFormat.Seconds); + Psychic.SetupCustomOption(); + Snitch.SetupCustomOption(); + Spiritualist.SetupCustomOption(); + TimeManager.SetupCustomOption(); + SetupRoleOptions(8950, TabGroup.CrewmateRoles, CustomRoles.TimeMaster); + TimeMasterSkillCooldown = FloatOptionItem.Create(8960, "TimeMasterSkillCooldown", new(1f, 180f, 1f), 20f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster]) + .SetValueFormat(OptionFormat.Seconds); + TimeMasterSkillDuration = FloatOptionItem.Create(8961, "TimeMasterSkillDuration", new(1f, 999f, 1f), 20f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.TimeMaster]) + .SetValueFormat(OptionFormat.Seconds); + + Tracker.SetupCustomOption(); + + TextOptionItem.Create(100008, "RoleType.CrewKilling", TabGroup.CrewmateRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(140, 255, 255, byte.MaxValue)); + SetupRoleOptions(8400, TabGroup.CrewmateRoles, CustomRoles.Bodyguard); + BodyguardProtectRadius = FloatOptionItem.Create(8410, "BodyguardProtectRadius", new(0.5f, 5f, 0.5f), 1.5f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bodyguard]) + .SetValueFormat(OptionFormat.Multiplier); + CopyCat.SetupCustomOption(); + Crusader.SetupCustomOption(); + Counterfeiter.SetupCustomOption(); + SwordsMan.SetupCustomOption(); + Reverie.SetupCustomOption(); + + SetupRoleOptions(8700, TabGroup.CrewmateRoles, CustomRoles.Retributionist); + RetributionistCanKillNum = IntegerOptionItem.Create(8710, "RetributionistCanKillNum", new(1, 15, 1), 1, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Retributionist]) + .SetValueFormat(OptionFormat.Players); + MinimumPlayersAliveToRetri = IntegerOptionItem.Create(8718, "MinimumPlayersAliveToRetri", new(0, 15, 1), 5, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Retributionist]) + .SetValueFormat(OptionFormat.Players); + CanOnlyRetributeWithTasksDone = BooleanOptionItem.Create(8715, "CanOnlyRetributeWithTasksDone", true, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Retributionist]); + RetributionistTasks = OverrideTasksData.Create(8720, TabGroup.CrewmateRoles, CustomRoles.Retributionist); + Sheriff.SetupCustomOption(); + SetupRoleOptions(8900, TabGroup.CrewmateRoles, CustomRoles.Veteran); + VeteranSkillCooldown = FloatOptionItem.Create(8910, "VeteranSkillCooldown", new(1f, 180f, 1f), 20f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Veteran]) + .SetValueFormat(OptionFormat.Seconds); + VeteranSkillDuration = FloatOptionItem.Create(8911, "VeteranSkillDuration", new(1f, 999f, 1f), 20f, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Veteran]) + .SetValueFormat(OptionFormat.Seconds); + VeteranSkillMaxOfUseage = IntegerOptionItem.Create(8912, "VeteranSkillMaxOfUseage", new(1, 999, 1), 10, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Veteran]) + .SetValueFormat(OptionFormat.Times); + SetupRoleOptions(8600, TabGroup.CrewmateRoles, CustomRoles.NiceGuesser); + GGCanGuessTime = IntegerOptionItem.Create(8610, "GuesserCanGuessTimes", new(1, 15, 1), 15, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.NiceGuesser]) + .SetValueFormat(OptionFormat.Times); + GGCanGuessCrew = BooleanOptionItem.Create(8611, "GGCanGuessCrew", true, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.NiceGuesser]); + GGCanGuessAdt = BooleanOptionItem.Create(8612, "GGCanGuessAdt", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.NiceGuesser]); + GGTryHideMsg = BooleanOptionItem.Create(8613, "GuesserTryHideMsg", true, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.NiceGuesser]) + .SetColor(Color.green); + + TextOptionItem.Create(100009, "RoleType.CrewPower", TabGroup.CrewmateRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(140, 255, 255, byte.MaxValue)); + SetupRoleOptions(9100, TabGroup.CrewmateRoles, CustomRoles.Dictator); + SetupRoleOptions(9200, TabGroup.CrewmateRoles, CustomRoles.Guardian); + GuardianTasks = OverrideTasksData.Create(9210, TabGroup.CrewmateRoles, CustomRoles.Guardian); + Judge.SetupCustomOption(); + Marshall.SetupCustomOption(); + SetupRoleOptions(9500, TabGroup.CrewmateRoles, CustomRoles.Mayor); + MayorAdditionalVote = IntegerOptionItem.Create(9510, "MayorAdditionalVote", new(1, 99, 1), 3, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mayor]) + .SetValueFormat(OptionFormat.Votes); + MayorHasPortableButton = BooleanOptionItem.Create(9511, "MayorHasPortableButton", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mayor]); + MayorNumOfUseButton = IntegerOptionItem.Create(9512, "MayorNumOfUseButton", new(1, 99, 1), 1, TabGroup.CrewmateRoles, false) + .SetParent(MayorHasPortableButton) + .SetValueFormat(OptionFormat.Times); + MayorHideVote = BooleanOptionItem.Create(9513, "MayorHideVote", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mayor]); + MayorRevealWhenDoneTasks = BooleanOptionItem.Create(9514, "MayorRevealWhenDoneTasks", false, TabGroup.CrewmateRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mayor]); + MayorTasks = OverrideTasksData.Create(9515, TabGroup.CrewmateRoles, CustomRoles.Mayor); + Monarch.SetupCustomOption(); + Farseer.SetupCustomOption(); + + // Neutral + TextOptionItem.Create(100010, "RoleType.NeutralBenign", TabGroup.NeutralRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(127, 140, 141, byte.MaxValue)); + Amnesiac.SetupCustomOption(); + Totocalcio.SetupCustomOption(); + SetupRoleOptions(11300, TabGroup.NeutralRoles, CustomRoles.FFF); + Lawyer.SetupCustomOption(); + Maverick.SetupCustomOption(); + SetupRoleOptions(10100, TabGroup.NeutralRoles, CustomRoles.Opportunist); + OppoImmuneToAttacksWhenTasksDone = BooleanOptionItem.Create(10110, "ImmuneToAttacksWhenTasksDone", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Opportunist]); + OpportunistTasks = OverrideTasksData.Create(10111, TabGroup.NeutralRoles, CustomRoles.Opportunist); + + Pursuer.SetupCustomOption(); + NWitch.SetupCustomOption(); + /* SetupSingleRoleOptions(6050530, TabGroup.NeutralRoles, CustomRoles.NWitch, 1, zeroOne: false); + ControlCooldown = FloatOptionItem.Create(6050532, "ControlCooldown", new(0f, 999f, 2.5f), 30f, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.NWitch]) + .SetValueFormat(OptionFormat.Seconds); */ + + TextOptionItem.Create(100011, "RoleType.NeutralEvil", TabGroup.NeutralRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(127, 140, 141, byte.MaxValue)); + + CursedSoul.SetupCustomOption(); + Gamer.SetupCustomOption(); + Executioner.SetupCustomOption(); + Doomsayer.SetupCustomOption(); + SetupRoleOptions(10800, TabGroup.NeutralRoles, CustomRoles.Innocent); + InnocentCanWinByImp = BooleanOptionItem.Create(10810, "InnocentCanWinByImp", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Innocent]); + SetupRoleOptions(10900, TabGroup.NeutralRoles, CustomRoles.Jester); + JesterCanUseButton = BooleanOptionItem.Create(10910, "JesterCanUseButton", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Jester]); + JesterCanVent = BooleanOptionItem.Create(10911, "CanVent", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Jester]); + JesterHasImpostorVision = BooleanOptionItem.Create(10913, "ImpostorVision", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Jester]); + SunnyboyChance = IntegerOptionItem.Create(10912, "SunnyboyChance", new(0, 100, 5), 0, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Jester]) + .SetValueFormat(OptionFormat.Percent); + SetupRoleOptions(10950, TabGroup.NeutralRoles, CustomRoles.Masochist); + MasochistKillMax = IntegerOptionItem.Create(10955, "MasochistKillMax", new(1, 99, 1), 5, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Masochist]) + .SetValueFormat(OptionFormat.Times); + + + TextOptionItem.Create(100012, "RoleType.NeutralChaos", TabGroup.NeutralRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(127, 140, 141, byte.MaxValue)); + // Baker.SetupCustomOption(); + Collector.SetupCustomOption(); + Succubus.SetupCustomOption(); + SetupRoleOptions(11400, TabGroup.NeutralRoles, CustomRoles.Phantom); + PhantomCanVent = BooleanOptionItem.Create(11410, "CanVent", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Phantom]); + PhantomSnatchesWin = BooleanOptionItem.Create(11411, "SnatchesWin", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Phantom]); + PhantomCanGuess = BooleanOptionItem.Create(11412, "CanGuess", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Phantom]); + PhantomTasks = OverrideTasksData.Create(11413, TabGroup.NeutralRoles, CustomRoles.Phantom); + Pirate.SetupCustomOption(); + SetupRoleOptions(11500, TabGroup.NeutralRoles, CustomRoles.Terrorist); + CanTerroristSuicideWin = BooleanOptionItem.Create(11510, "CanTerroristSuicideWin", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Terrorist]); + TerroristCanGuess = BooleanOptionItem.Create(11511, "CanGuess", true, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Terrorist]); + //50220~50223を使用 + TerroristTasks = OverrideTasksData.Create(11512, TabGroup.NeutralRoles, CustomRoles.Terrorist); + Vulture.SetupCustomOption(); + SetupRoleOptions(11700, TabGroup.NeutralRoles, CustomRoles.Workaholic); //TOH_Y + WorkaholicCannotWinAtDeath = BooleanOptionItem.Create(11710, "WorkaholicCannotWinAtDeath", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Workaholic]); + WorkaholicVentCooldown = FloatOptionItem.Create(11711, "VentCooldown", new(0f, 180f, 2.5f), 0f, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Workaholic]) + .SetValueFormat(OptionFormat.Seconds); + WorkaholicVisibleToEveryone = BooleanOptionItem.Create(11712, "WorkaholicVisibleToEveryone", true, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Workaholic]); + WorkaholicGiveAdviceAlive = BooleanOptionItem.Create(11713, "WorkaholicGiveAdviceAlive", true, TabGroup.NeutralRoles, false) + .SetParent(WorkaholicVisibleToEveryone); + WorkaholicTasks = OverrideTasksData.Create(11714, TabGroup.NeutralRoles, CustomRoles.Workaholic); + WorkaholicCanGuess = BooleanOptionItem.Create(11725, "CanGuess", true, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Workaholic]); + + TextOptionItem.Create(100013, "RoleType.NeutralKilling", TabGroup.NeutralRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(127, 140, 141, byte.MaxValue)); + + SetupRoleOptions(10400, TabGroup.NeutralRoles, CustomRoles.Arsonist); + ArsonistDouseTime = FloatOptionItem.Create(10410, "ArsonistDouseTime", new(0f, 10f, 1f), 3f, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Arsonist]) + .SetValueFormat(OptionFormat.Seconds); + ArsonistCooldown = FloatOptionItem.Create(10411, "Cooldown", new(0f, 990f, 1f), 10f, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Arsonist]) + .SetValueFormat(OptionFormat.Seconds); + ArsonistKeepsGameGoing = BooleanOptionItem.Create(10412, "ArsonistKeepsGameGoing", false, TabGroup.NeutralRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Arsonist]); + BloodKnight.SetupCustomOption(); + Glitch.SetupCustomOption(); + HexMaster.SetupCustomOption(); + Infectious.SetupCustomOption(); + Jackal.SetupCustomOption(); + Jinx.SetupCustomOption(); + Juggernaut.SetupCustomOption(); + Medusa.SetupCustomOption(); + Pelican.SetupCustomOption(); + Pickpocket.SetupCustomOption(); + PlagueBearer.SetupCustomOption(); + Poisoner.SetupCustomOption(); + NSerialKiller.SetupCustomOption(); + DarkHide.SetupCustomOption(); //TOH_Y + Ritualist.SetupCustomOption(); + Traitor.SetupCustomOption(); + Virus.SetupCustomOption(); + Wraith.SetupCustomOption(); + + + + // Add-Ons + AddBracketsToAddons = BooleanOptionItem.Create(13500, "BracketAddons", false, TabGroup.Addons, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true); + + TextOptionItem.Create(100014, "RoleType.Helpful", TabGroup.Addons) // HELPFUL + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 154, 206, byte.MaxValue)); + + SetupAdtRoleOptions(222420, CustomRoles.Antidote, canSetNum: true); + ImpCanBeAntidote = BooleanOptionItem.Create(222426, "ImpCanBeAntidote", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Antidote]); + CrewCanBeAntidote = BooleanOptionItem.Create(222427, "CrewCanBeAntidote", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Antidote]); + NeutralCanBeAntidote = BooleanOptionItem.Create(222423, "NeutralCanBeAntidote", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Antidote]); + AntidoteCDOpt = FloatOptionItem.Create(222424, "AntidoteCDOpt", new(0f, 999f, 1f), 5f, TabGroup.Addons, false) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Antidote]) + .SetValueFormat(OptionFormat.Seconds); + AntidoteCDReset = BooleanOptionItem.Create(222425, "AntidoteCDReset", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Antidote]); + SetupAdtRoleOptions(13600, CustomRoles.Autopsy, canSetNum: true); + ImpCanBeAutopsy = BooleanOptionItem.Create(13610, "ImpCanBeAutopsy", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Autopsy]); + CrewCanBeAutopsy = BooleanOptionItem.Create(13611, "CrewCanBeAutopsy", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Autopsy]); + NeutralCanBeAutopsy = BooleanOptionItem.Create(13612, "NeutralCanBeAutopsy", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Autopsy]); + SetupAdtRoleOptions(13700, CustomRoles.Bait, canSetNum: true); + ImpCanBeBait = BooleanOptionItem.Create(13710, "ImpCanBeBait", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]); + CrewCanBeBait = BooleanOptionItem.Create(13711, "CrewCanBeBait", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]); + NeutralCanBeBait = BooleanOptionItem.Create(13712, "NeutralCanBeBait", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]); + BaitDelayMin = FloatOptionItem.Create(13713, "BaitDelayMin", new(0f, 5f, 1f), 0f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]) + .SetValueFormat(OptionFormat.Seconds); + BaitDelayMax = FloatOptionItem.Create(13714, "BaitDelayMax", new(0f, 10f, 1f), 0f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]) + .SetValueFormat(OptionFormat.Seconds); + BaitDelayNotify = BooleanOptionItem.Create(13715, "BaitDelayNotify", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]); + BaitNotification = BooleanOptionItem.Create(13716, "BaitNotification", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bait]); + SetupAdtRoleOptions(13800, CustomRoles.Trapper, canSetNum: true); + ImpCanBeTrapper = BooleanOptionItem.Create(13810, "ImpCanBeTrapper", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Trapper]); + CrewCanBeTrapper = BooleanOptionItem.Create(13811, "CrewCanBeTrapper", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Trapper]); + NeutralCanBeTrapper = BooleanOptionItem.Create(13812, "NeutralCanBeTrapper", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Trapper]); + TrapperBlockMoveTime = FloatOptionItem.Create(13813, "TrapperBlockMoveTime", new(1f, 180f, 1f), 5f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Trapper]) + .SetValueFormat(OptionFormat.Seconds); + SetupAdtRoleOptions(13900, CustomRoles.DoubleShot, canSetNum: true, tab: TabGroup.Addons); + ImpCanBeDoubleShot = BooleanOptionItem.Create(13910, "ImpCanBeDoubleShot", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DoubleShot]); + CrewCanBeDoubleShot = BooleanOptionItem.Create(13911, "CrewCanBeDoubleShot", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DoubleShot]); + NeutralCanBeDoubleShot = BooleanOptionItem.Create(13912, "NeutralCanBeDoubleShot", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DoubleShot]); + SetupAdtRoleOptions(14020, CustomRoles.Glow, canSetNum: true); + ImpCanBeGlow = BooleanOptionItem.Create(14030, "ImpCanBeGlow", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Glow]); + CrewCanBeGlow = BooleanOptionItem.Create(14031, "CrewCanBeGlow", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Glow]); + NeutralCanBeGlow = BooleanOptionItem.Create(14032, "NeutralCanBeGlow", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Glow]); + SetupAdtRoleOptions(14000, CustomRoles.Gravestone, canSetNum: true); + ImpCanBeGravestone = BooleanOptionItem.Create(14010, "ImpCanBeGravestone", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Gravestone]); + CrewCanBeGravestone = BooleanOptionItem.Create(14011, "CrewCanBeGravestone", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Gravestone]); + NeutralCanBeGravestone = BooleanOptionItem.Create(14012, "NeutralCanBeGravestone", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Gravestone]); + + SetupAdtRoleOptions(14100, CustomRoles.Lazy, canSetNum: true); + TasklessCrewCanBeLazy = BooleanOptionItem.Create(14110, "TasklessCrewCanBeLazy", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lazy]); + TaskBasedCrewCanBeLazy = BooleanOptionItem.Create(14120, "TaskBasedCrewCanBeLazy", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lazy]); + SetupAdtRoleOptions(14200, CustomRoles.Lighter, canSetNum: true); + LighterVision = FloatOptionItem.Create(14210, "LighterVision", new(0.5f, 5f, 0.25f), 1.25f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lighter]) + .SetValueFormat(OptionFormat.Multiplier); + SetupAdtRoleOptions(15500, CustomRoles.Loyal, canSetNum: true); + ImpCanBeLoyal = BooleanOptionItem.Create(15510, "ImpCanBeLoyal", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Loyal]); + CrewCanBeLoyal = BooleanOptionItem.Create(15511, "CrewCanBeLoyal", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Loyal]); + SetupAdtRoleOptions(14300, CustomRoles.Lucky, canSetNum: true); + LuckyProbability = IntegerOptionItem.Create(14310, "LuckyProbability", new(0, 100, 5), 50, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lucky]) + .SetValueFormat(OptionFormat.Percent); + ImpCanBeLucky = BooleanOptionItem.Create(14311, "ImpCanBeLucky", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lucky]); + CrewCanBeLucky = BooleanOptionItem.Create(14312, "CrewCanBeLucky", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lucky]); + NeutralCanBeLucky = BooleanOptionItem.Create(14313, "NeutralCanBeLucky", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Lucky]); + SetupAdtRoleOptions(14400, CustomRoles.Necroview, canSetNum: true, tab: TabGroup.Addons); + ImpCanBeNecroview = BooleanOptionItem.Create(14410, "ImpCanBeNecroview", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Necroview]); + CrewCanBeNecroview = BooleanOptionItem.Create(14411, "CrewCanBeNecroview", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Necroview]); + NeutralCanBeNecroview = BooleanOptionItem.Create(14412, "NeutralCanBeNecroview", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Necroview]); + SetupAdtRoleOptions(14600, CustomRoles.Reach, canSetNum: true); + SetupAdtRoleOptions(14700, CustomRoles.DualPersonality, canSetNum: true); + ImpCanBeDualPersonality = BooleanOptionItem.Create(14710, "ImpCanBeDualPersonality", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DualPersonality]); + CrewCanBeDualPersonality = BooleanOptionItem.Create(14711, "CrewCanBeDualPersonality", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DualPersonality]); + DualVotes = BooleanOptionItem.Create(14712, "DualVotes", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.DualPersonality]); + // HideDualVotes = BooleanOptionItem.Create(14713, "HideDualVotes", true, TabGroup.Addons, false) + // .SetParent(DualVotes); + SetupAdtRoleOptions(14800, CustomRoles.Seer, canSetNum: true); + ImpCanBeSeer = BooleanOptionItem.Create(14810, "ImpCanBeSeer", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Seer]); + CrewCanBeSeer = BooleanOptionItem.Create(14811, "CrewCanBeSeer", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Seer]); + NeutralCanBeSeer = BooleanOptionItem.Create(14812, "NeutralCanBeSeer", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Seer]); + SetupAdtRoleOptions(14900, CustomRoles.Brakar, canSetNum: true); + ImpCanBeTiebreaker = BooleanOptionItem.Create(14910, "ImpCanBeTiebreaker", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Brakar]); + CrewCanBeTiebreaker = BooleanOptionItem.Create(14911, "CrewCanBeTiebreaker", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Brakar]); + NeutralCanBeTiebreaker = BooleanOptionItem.Create(14912, "NeutralCanBeTiebreaker", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Brakar]); + SetupAdtRoleOptions(15000, CustomRoles.Watcher, canSetNum: true); + ImpCanBeWatcher = BooleanOptionItem.Create(15010, "ImpCanBeWatcher", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Watcher]); + CrewCanBeWatcher = BooleanOptionItem.Create(15011, "CrewCanBeWatcher", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Watcher]); + NeutralCanBeWatcher = BooleanOptionItem.Create(15012, "NeutralCanBeWatcher", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Watcher]); + + TextOptionItem.Create(100015, "RoleType.Harmful", TabGroup.Addons) // HARMFUL + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 154, 206, byte.MaxValue)); + + SetupAdtRoleOptions(15100, CustomRoles.Avanger, canSetNum: true); + ImpCanBeAvanger = BooleanOptionItem.Create(15110, "ImpCanBeAvanger", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Avanger]); + SetupAdtRoleOptions(15200, CustomRoles.Bewilder, canSetNum: true); + BewilderVision = FloatOptionItem.Create(15210, "BewilderVision", new(0f, 5f, 0.05f), 0.6f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bewilder]) + .SetValueFormat(OptionFormat.Multiplier); + ImpCanBeBewilder = BooleanOptionItem.Create(15211, "ImpCanBeBewilder", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bewilder]); + CrewCanBeBewilder = BooleanOptionItem.Create(15212, "CrewCanBeBewilder", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bewilder]); + NeutralCanBeBewilder = BooleanOptionItem.Create(15213, "NeutralCanBeBewilder", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Bewilder]); + /* SetupAdtRoleOptions(15350, CustomRoles.Diseased, canSetNum: true); + DiseasedMultiplier = FloatOptionItem.Create(15363, "DiseasedMultiplier", new(1.5f, 5f, 0.1f), 2f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]) + .SetValueFormat(OptionFormat.Multiplier); + ImpCanBeDiseased = BooleanOptionItem.Create(15360, "ImpCanBeDiseased", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); + CrewCanBeDiseased = BooleanOptionItem.Create(15361, "CrewCanBeDiseased", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); + NeutralCanBeDiseased = BooleanOptionItem.Create(15362, "NeutralCanBeDiseased", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); */ + SetupAdtRoleOptions(111420, CustomRoles.Diseased, canSetNum: true); + ImpCanBeDiseased = BooleanOptionItem.Create(111426, "ImpCanBeDiseased", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); + CrewCanBeDiseased = BooleanOptionItem.Create(111427, "CrewCanBeDiseased", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); + NeutralCanBeDiseased = BooleanOptionItem.Create(111423, "NeutralCanBeDiseased", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); + DiseasedCDOpt = FloatOptionItem.Create(111424, "DiseasedCDOpt", new(0f, 999f, 1f), 25f, TabGroup.Addons, false) + .SetParent(Options.CustomRoleSpawnChances[CustomRoles.Diseased]) + .SetValueFormat(OptionFormat.Seconds); + DiseasedCDReset = BooleanOptionItem.Create(111425, "DiseasedCDReset", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Diseased]); + SetupAdtRoleOptions(15300, CustomRoles.Unreportable, canSetNum: true); + ImpCanBeUnreportable = BooleanOptionItem.Create(15310, "ImpCanBeUnreportable", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unreportable]); + CrewCanBeUnreportable = BooleanOptionItem.Create(15311, "CrewCanBeUnreportable", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unreportable]); + NeutralCanBeUnreportable = BooleanOptionItem.Create(15312, "NeutralCanBeUnreportable", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unreportable]); + SetupAdtRoleOptions(15250, CustomRoles.Ghoul, canSetNum: true); + SetupAdtRoleOptions(15400, CustomRoles.Oblivious, canSetNum: true); + ImpCanBeOblivious = BooleanOptionItem.Create(15410, "ImpCanBeOblivious", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Oblivious]); + CrewCanBeOblivious = BooleanOptionItem.Create(15411, "CrewCanBeOblivious", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Oblivious]); + NeutralCanBeOblivious = BooleanOptionItem.Create(15412, "NeutralCanBeOblivious", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Oblivious]); + ObliviousBaitImmune = BooleanOptionItem.Create(15413, "ObliviousBaitImmune", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Oblivious]); + SetupAdtRoleOptions(15600, CustomRoles.Rascal, canSetNum: true, tab: TabGroup.Addons); + RascalAppearAsMadmate = BooleanOptionItem.Create(15610, "RascalAppearAsMadmate", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Rascal]); + SetupAdtRoleOptions(15450, CustomRoles.Sunglasses, canSetNum: true); + SunglassesVision = FloatOptionItem.Create(15460, "SunglassesVision", new(0f, 5f, 0.05f), 0.75f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sunglasses]) + .SetValueFormat(OptionFormat.Multiplier); + ImpCanBeSunglasses = BooleanOptionItem.Create(15461, "ImpCanBeSunglasses", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sunglasses]); + CrewCanBeSunglasses = BooleanOptionItem.Create(15462, "CrewCanBeSunglasses", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sunglasses]); + NeutralCanBeSunglasses = BooleanOptionItem.Create(15463, "NeutralCanBeSunglasses", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sunglasses]); + SetupAdtRoleOptions(14350, CustomRoles.Unlucky, canSetNum: true); + UnluckyKillSuicideChance = IntegerOptionItem.Create(14364, "UnluckyKillSuicideChance", new(0, 100, 1), 2, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]) + .SetValueFormat(OptionFormat.Percent); + UnluckyTaskSuicideChance = IntegerOptionItem.Create(14365, "UnluckyTaskSuicideChance", new(0, 100, 1), 5, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]) + .SetValueFormat(OptionFormat.Percent); + UnluckyVentSuicideChance = IntegerOptionItem.Create(14366, "UnluckyVentSuicideChance", new(0, 100, 1), 3, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]) + .SetValueFormat(OptionFormat.Percent); + UnluckyReportSuicideChance = IntegerOptionItem.Create(14367, "UnluckyReportSuicideChance", new(0, 100, 1), 1, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]) + .SetValueFormat(OptionFormat.Percent); + UnluckySabotageSuicideChance = IntegerOptionItem.Create(14368, "UnluckySabotageSuicideChance", new(0, 100, 1), 4, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]) + .SetValueFormat(OptionFormat.Percent); + ImpCanBeUnlucky = BooleanOptionItem.Create(14361, "ImpCanBeUnlucky", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]); + CrewCanBeUnlucky = BooleanOptionItem.Create(14362, "CrewCanBeUnlucky", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]); + NeutralCanBeUnlucky = BooleanOptionItem.Create(14363, "NeutralCanBeUnlucky", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Unlucky]); + Workhorse.SetupCustomOption(); + + TextOptionItem.Create(100016, "RoleType.Impostor", TabGroup.Addons) // IMPOSTOR + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 25, 25, byte.MaxValue)); + SetupAdtRoleOptions(15800, CustomRoles.Madmate, canSetNum: true, canSetChance: false); + MadmateSpawnMode = StringOptionItem.Create(15810, "MadmateSpawnMode", madmateSpawnMode, 0, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + MadmateCountMode = StringOptionItem.Create(15811, "MadmateCountMode", madmateCountMode, 0, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + SheriffCanBeMadmate = BooleanOptionItem.Create(15812, "SheriffCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + MayorCanBeMadmate = BooleanOptionItem.Create(15813, "MayorCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + NGuesserCanBeMadmate = BooleanOptionItem.Create(15814, "NGuesserCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + MarshallCanBeMadmate = BooleanOptionItem.Create(15815, "MarshallCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + FarseerCanBeMadmate = BooleanOptionItem.Create(15816, "FarseerCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + RetributionistCanBeMadmate = BooleanOptionItem.Create(15817, "RetributionistCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + SnitchCanBeMadmate = BooleanOptionItem.Create(15818, "SnitchCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + MadSnitchTasks = IntegerOptionItem.Create(15819, "MadSnitchTasks", new(1, 99, 1), 3, TabGroup.Addons, false) + .SetParent(SnitchCanBeMadmate) + .SetValueFormat(OptionFormat.Pieces); + JudgeCanBeMadmate = BooleanOptionItem.Create(15820, "JudgeCanBeMadmate", false, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Madmate]); + LastImpostor.SetupCustomOption(); + SetupAdtRoleOptions(1600, CustomRoles.Mare, canSetNum: true, tab: TabGroup.Addons); + MareKillCD = FloatOptionItem.Create(1605, "KillCooldown", new(0f, 99f, 1f), 10f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mare]) + .SetValueFormat(OptionFormat.Seconds); + SetupAdtRoleOptions(16000, CustomRoles.Mimic, canSetNum: true, tab: TabGroup.Addons); + MimicCanSeeDeadRoles = BooleanOptionItem.Create(16010, "MimicCanSeeDeadRoles", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mimic]); + SetupAdtRoleOptions(16100, CustomRoles.TicketsStealer, canSetNum: true, tab: TabGroup.Addons); + TicketsPerKill = FloatOptionItem.Create(16110, "TicketsPerKill", new(0.1f, 10f, 0.1f), 0.5f, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.TicketsStealer]); + SetupAdtRoleOptions(16050, CustomRoles.Swift, canSetNum: true, tab: TabGroup.Addons); + // SetupAdtRoleOptions(16300, CustomRoles.Minimalism, canSetNum: true, tab: TabGroup.Addons); + + + TextOptionItem.Create(100017, "RoleType.Neut", TabGroup.Addons) // NEUTRAL + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(127, 140, 141, byte.MaxValue)); + SetupLoversRoleOptionsToggle(16200); + + + + + + /* SetupAdtRoleOptions(6050900, CustomRoles.Rogue, canSetNum: true); + ImpCanBeRogue = BooleanOptionItem.Create(6050903, "ImpCanBeRogue", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Rogue]); + CrewCanBeRogue = BooleanOptionItem.Create(6050904, "CrewCanBeRogue", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Rogue]); + NeutralCanBeRogue = BooleanOptionItem.Create(6050905, "NeutralCanBeRogue", true, TabGroup.Addons, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Rogue]); + RogueKnowEachOther = BooleanOptionItem.Create(6050906, "RogueKnowEachOther", false, TabGroup.Addons, false).SetParent(CustomRoleSpawnChances[CustomRoles.Rogue]); + RogueKnowEachOtherRoles = BooleanOptionItem.Create(6050907, "RogueKnowEachOtherRoles", false, TabGroup.Addons, false).SetParent(RogueKnowEachOther); + */ + + + + + + // 乐子职业 + + // 内鬼 + TextOptionItem.Create(100018, "OtherRoles.ImpostorRoles", TabGroup.OtherRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(247, 70, 49, byte.MaxValue)); + + SetupRoleOptions(16300, TabGroup.OtherRoles, CustomRoles.Minimalism); + MNKillCooldown = FloatOptionItem.Create(16310, "KillCooldown", new(2.5f, 999f, 2.5f), 10f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Minimalism]) + .SetValueFormat(OptionFormat.Seconds); + SetupRoleOptions(16400, TabGroup.OtherRoles, CustomRoles.Zombie); + ZombieKillCooldown = FloatOptionItem.Create(16410, "KillCooldown", new(0f, 999f, 2.5f), 5f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Zombie]) + .SetValueFormat(OptionFormat.Seconds); + ZombieSpeedReduce = FloatOptionItem.Create(16411, "ZombieSpeedReduce", new(0.0f, 1.0f, 0.1f), 0.1f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Zombie]) + .SetValueFormat(OptionFormat.Multiplier); + SetupRoleOptions(16500, TabGroup.OtherRoles, CustomRoles.BoobyTrap); + BTKillCooldown = FloatOptionItem.Create(16510, "KillCooldown", new(2.5f, 999f, 2.5f), 20f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.BoobyTrap]) + .SetValueFormat(OptionFormat.Seconds); + TrapOnlyWorksOnTheBodyBoobyTrap = BooleanOptionItem.Create(16511, "TrapOnlyWorksOnTheBodyBoobyTrap", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.BoobyTrap]); + SetupRoleOptions(16600, TabGroup.OtherRoles, CustomRoles.Capitalism); + CapitalismSkillCooldown = FloatOptionItem.Create(16610, "CapitalismSkillCooldown", new(2.5f, 900f, 2.5f), 20f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Capitalism]) + .SetValueFormat(OptionFormat.Seconds); + BallLightning.SetupCustomOption(); + Eraser.SetupCustomOption(); + SetupRoleOptions(16900, TabGroup.OtherRoles, CustomRoles.OverKiller); + Disperser.SetupCustomOption(); + + // 船员 + TextOptionItem.Create(100019, "OtherRoles.CrewmateRoles", TabGroup.OtherRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(140, 255, 255, byte.MaxValue)); + /* SetupRoleOptions(18000, TabGroup.OtherRoles, CustomRoles.SpeedBooster); + SpeedBoosterUpSpeed = FloatOptionItem.Create(18010, "SpeedBoosterUpSpeed", new(0.1f, 1.0f, 0.1f), 0.2f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.SpeedBooster]) + .SetValueFormat(OptionFormat.Multiplier); + SpeedBoosterTimes = IntegerOptionItem.Create(18011, "SpeedBoosterTimes", new(1, 99, 1), 5, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.SpeedBooster]) + .SetValueFormat(OptionFormat.Times); */ + /* SetupRoleOptions(18100, TabGroup.OtherRoles, CustomRoles.Glitch); + GlitchCanVote = BooleanOptionItem.Create(18110, "GlitchCanVote", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Glitch]); */ + // Divinator.SetupCustomOption(); + // 中立 + TextOptionItem.Create(100020, "OtherRoles.NeutralRoles", TabGroup.OtherRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(127, 140, 141, byte.MaxValue)); + SetupRoleOptions(18200, TabGroup.OtherRoles, CustomRoles.God); + NotifyGodAlive = BooleanOptionItem.Create(18210, "NotifyGodAlive", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.God]); + GodCanGuess = BooleanOptionItem.Create(18211, "CanGuess", false, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.God]); + SetupRoleOptions(18300, TabGroup.OtherRoles, CustomRoles.Mario); + MarioVentNumWin = IntegerOptionItem.Create(18310, "MarioVentNumWin", new(5, 900, 5), 40, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mario]) + .SetValueFormat(OptionFormat.Times); + MarioVentCD = FloatOptionItem.Create(18311, "VentCooldown", new(0f, 999f, 1f), 15f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Mario]) + .SetValueFormat(OptionFormat.Seconds); + SetupRoleOptions(18400, TabGroup.OtherRoles, CustomRoles.Revolutionist); + RevolutionistDrawTime = FloatOptionItem.Create(18410, "RevolutionistDrawTime", new(0f, 10f, 1f), 3f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Revolutionist]) + .SetValueFormat(OptionFormat.Seconds); + RevolutionistCooldown = FloatOptionItem.Create(18411, "RevolutionistCooldown", new(5f, 100f, 1f), 10f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Revolutionist]) + .SetValueFormat(OptionFormat.Seconds); + RevolutionistDrawCount = IntegerOptionItem.Create(18412, "RevolutionistDrawCount", new(1, 14, 1), 6, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Revolutionist]) + .SetValueFormat(OptionFormat.Players); + RevolutionistKillProbability = IntegerOptionItem.Create(18413, "RevolutionistKillProbability", new(0, 100, 5), 15, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Revolutionist]) + .SetValueFormat(OptionFormat.Percent); + RevolutionistVentCountDown = FloatOptionItem.Create(18414, "RevolutionistVentCountDown", new(1f, 180f, 1f), 15f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Revolutionist]) + .SetValueFormat(OptionFormat.Seconds); + SetupRoleOptions(18500, TabGroup.OtherRoles, CustomRoles.Provocateur); + Spiritcaller.SetupCustomOption(); + + // 副职 + TextOptionItem.Create(100021, "OtherRoles.Addons", TabGroup.OtherRoles) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 154, 206, byte.MaxValue)); + + SetupAdtRoleOptions(18600, CustomRoles.Ntr, tab: TabGroup.OtherRoles); + /* SetupAdtRoleOptions(18700, CustomRoles.Flashman, canSetNum: true, tab: TabGroup.OtherRoles); + FlashmanSpeed = FloatOptionItem.Create(6050335, "FlashmanSpeed", new(0.25f, 5f, 0.25f), 2.5f, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Flashman]) + .SetValueFormat(OptionFormat.Multiplier); */ + SetupAdtRoleOptions(18800, CustomRoles.Youtuber, canSetNum: true, tab: TabGroup.OtherRoles); + SetupAdtRoleOptions(18900, CustomRoles.Egoist, canSetNum: true, tab: TabGroup.OtherRoles); + CrewCanBeEgoist = BooleanOptionItem.Create(18910, "CrewCanBeEgoist", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Egoist]); + ImpCanBeEgoist = BooleanOptionItem.Create(18911, "ImpCanBeEgoist", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Egoist]); + ImpEgoistVisibalToAllies = BooleanOptionItem.Create(18912, "ImpEgoistVisibalToAllies", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Egoist]); + /* SetupAdtRoleOptions(19000, CustomRoles.Sidekick, canSetNum: true, tab: TabGroup.OtherRoles); + SidekickCountMode = StringOptionItem.Create(19010, "SidekickCountMode", sidekickCountMode, 0, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + CrewmateCanBeSidekick = BooleanOptionItem.Create(19011, "CrewmatesCanBeSidekick", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + NeutralCanBeSidekick = BooleanOptionItem.Create(19012, "NeutralsCanBeSidekick", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + ImpostorCanBeSidekick = BooleanOptionItem.Create(19013, "ImpostorsCanBeSidekick", false, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + SidekickCanKillJackal = BooleanOptionItem.Create(19014, "SidekickCanKillJackal", false, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + // JackalWinWithSidekick = BooleanOptionItem.Create(19015, "JackalWinWithSidekick", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + SidekickKnowOtherSidekick = BooleanOptionItem.Create(19016, "SidekickKnowOtherSidekick", false, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Sidekick]); + SidekickKnowOtherSidekickRole = BooleanOptionItem.Create(19017, "SidekickKnowOtherSidekickRole", false, TabGroup.OtherRoles, false) + .SetParent(SidekickKnowOtherSidekick); + SidekickCanKillSidekick = BooleanOptionItem.Create(19018, "SidekickCanKillSidekick", false, TabGroup.OtherRoles, false) + .SetParent(SidekickKnowOtherSidekick); */ + SetupAdtRoleOptions(19100, CustomRoles.Guesser, canSetNum: true, tab: TabGroup.OtherRoles); + ImpCanBeGuesser = BooleanOptionItem.Create(19110, "ImpCanBeGuesser", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + CrewCanBeGuesser = BooleanOptionItem.Create(19111, "CrewCanBeGuesser", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + NeutralCanBeGuesser = BooleanOptionItem.Create(19112, "NeutralCanBeGuesser", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + // GCanGuessImp = BooleanOptionItem.Create(19113, "GCanGuessImp", false, TabGroup.OtherRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + // GCanGuessCrew = BooleanOptionItem.Create(19114, "GCanGuessCrew", false, TabGroup.OtherRoles, false).SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + GCanGuessAdt = BooleanOptionItem.Create(19115, "GCanGuessAdt", false, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + GCanGuessTaskDoneSnitch = BooleanOptionItem.Create(19116, "GCanGuessTaskDoneSnitch", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]); + GTryHideMsg = BooleanOptionItem.Create(19117, "GuesserTryHideMsg", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Guesser]) + .SetColor(Color.green); + SetupAdtRoleOptions(19200, CustomRoles.Fool, canSetNum: true, tab: TabGroup.OtherRoles); + ImpCanBeFool = BooleanOptionItem.Create(19210, "ImpCanBeFool", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Fool]); + CrewCanBeFool = BooleanOptionItem.Create(19211, "CrewCanBeFool", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Fool]); + NeutralCanBeFool = BooleanOptionItem.Create(19212, "NeutralCanBeFool", true, TabGroup.OtherRoles, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Fool]); + + + + #endregion + + #region 系统设置 + + KickLowLevelPlayer = IntegerOptionItem.Create(19300, "KickLowLevelPlayer", new(0, 100, 1), 0, TabGroup.SystemSettings, false) + .SetValueFormat(OptionFormat.Level) + .SetHeader(true); + KickAndroidPlayer = BooleanOptionItem.Create(19301, "KickAndroidPlayer", false, TabGroup.SystemSettings, false); + KickPlayerFriendCodeNotExist = BooleanOptionItem.Create(19302, "KickPlayerFriendCodeNotExist", false, TabGroup.SystemSettings, true); + ApplyDenyNameList = BooleanOptionItem.Create(19303, "ApplyDenyNameList", true, TabGroup.SystemSettings, true); + ApplyBanList = BooleanOptionItem.Create(19304, "ApplyBanList", true, TabGroup.SystemSettings, true); + ApplyModeratorList = BooleanOptionItem.Create(19305, "ApplyModeratorList", false, TabGroup.SystemSettings, false); + // ApplyAllowList = BooleanOptionItem.Create(19306, "ApplyAllowList", false, TabGroup.SystemSettings, false); + // AllowSayCommand = BooleanOptionItem.Create(19307, "AllowSayCommand", false, TabGroup.SystemSettings, false); + // ApplyReminderMsg = BooleanOptionItem.Create(19308, "ApplyReminderMsg", false, TabGroup.SystemSettings, false); + /* TimeForReminder = IntegerOptionItem.Create(19309, "TimeForReminder", new(0, 99, 1), 3, TabGroup.SystemSettings, false) + .SetParent(TimeForReminder) + .SetValueFormat(OptionFormat.Seconds); */ + AutoKickStart = BooleanOptionItem.Create(19310, "AutoKickStart", false, TabGroup.SystemSettings, false); + AutoKickStartTimes = IntegerOptionItem.Create(19311, "AutoKickStartTimes", new(0, 99, 1), 1, TabGroup.SystemSettings, false) + .SetParent(AutoKickStart) + .SetValueFormat(OptionFormat.Times); + AutoKickStartAsBan = BooleanOptionItem.Create(19312, "AutoKickStartAsBan", false, TabGroup.SystemSettings, false) + .SetParent(AutoKickStart); + AutoKickStopWords = BooleanOptionItem.Create(19313, "AutoKickStopWords", false, TabGroup.SystemSettings, false); + AutoKickStopWordsTimes = IntegerOptionItem.Create(19314, "AutoKickStopWordsTimes", new(0, 99, 1), 3, TabGroup.SystemSettings, false) + .SetParent(AutoKickStopWords) + .SetValueFormat(OptionFormat.Times); + AutoKickStopWordsAsBan = BooleanOptionItem.Create(19315, "AutoKickStopWordsAsBan", false, TabGroup.SystemSettings, false) + .SetParent(AutoKickStopWords); + AutoWarnStopWords = BooleanOptionItem.Create(19316, "AutoWarnStopWords", false, TabGroup.SystemSettings, false); + MinWaitAutoStart = FloatOptionItem.Create(44420, "MinWaitAutoStart", new(0f, 10f, 0.5f), 1.5f, TabGroup.SystemSettings, false); + MaxWaitAutoStart = FloatOptionItem.Create(44421, "MaxWaitAutoStart", new(0f, 10f, 0.5f), 1.5f, TabGroup.SystemSettings, false); + PlayerAutoStart = IntegerOptionItem.Create(44422, "PlayerAutoStart", new(1,15,1), 14, TabGroup.SystemSettings, false); + + LowLoadMode = BooleanOptionItem.Create(19317, "LowLoadMode", false, TabGroup.SystemSettings, false) + .SetHeader(true) + .SetColor(Color.red); + + EndWhenPlayerBug = BooleanOptionItem.Create(19318, "EndWhenPlayerBug", true, TabGroup.SystemSettings, false) + .SetHeader(true) + .SetColor(Color.blue); + + CheatResponses = StringOptionItem.Create(19319, "CheatResponses", CheatResponsesName, 0, TabGroup.SystemSettings, false) + .SetHeader(true); + + //HighLevelAntiCheat = StringOptionItem.Create(19320, "HighLevelAntiCheat", CheatResponsesName, 0, TabGroup.SystemSettings, false) + //.SetHeader(true); + + AutoDisplayKillLog = BooleanOptionItem.Create(19321, "AutoDisplayKillLog", true, TabGroup.SystemSettings, false) + .SetHeader(true); + AutoDisplayLastRoles = BooleanOptionItem.Create(19322, "AutoDisplayLastRoles", true, TabGroup.SystemSettings, false); + AutoDisplayLastResult = BooleanOptionItem.Create(19323, "AutoDisplayLastResult", true, TabGroup.SystemSettings, false); + + SuffixMode = StringOptionItem.Create(19324, "SuffixMode", suffixModes, 0, TabGroup.SystemSettings, true) + .SetHeader(true); + HideGameSettings = BooleanOptionItem.Create(19400, "HideGameSettings", false, TabGroup.SystemSettings, false); + DIYGameSettings = BooleanOptionItem.Create(19401, "DIYGameSettings", false, TabGroup.SystemSettings, false); + PlayerCanSetColor = BooleanOptionItem.Create(19402, "PlayerCanSetColor", false, TabGroup.SystemSettings, false); + FormatNameMode = StringOptionItem.Create(19403, "FormatNameMode", formatNameModes, 0, TabGroup.SystemSettings, false); + DisableEmojiName = BooleanOptionItem.Create(19404, "DisableEmojiName", true, TabGroup.SystemSettings, false); + ChangeNameToRoleInfo = BooleanOptionItem.Create(19405, "ChangeNameToRoleInfo", false, TabGroup.SystemSettings, false); + SendRoleDescriptionFirstMeeting = BooleanOptionItem.Create(19406, "SendRoleDescriptionFirstMeeting", false, TabGroup.SystemSettings, false); + NoGameEnd = BooleanOptionItem.Create(19407, "NoGameEnd", false, TabGroup.SystemSettings, false) + .SetColor(Color.red); + AllowConsole = BooleanOptionItem.Create(19408, "AllowConsole", false, TabGroup.SystemSettings, false) + .SetColor(Color.red); + RoleAssigningAlgorithm = StringOptionItem.Create(19409, "RoleAssigningAlgorithm", roleAssigningAlgorithms, 4, TabGroup.SystemSettings, true) + .RegisterUpdateValueEvent( + (object obj, OptionItem.UpdateValueEventArgs args) => IRandom.SetInstanceById(args.CurrentValue) + ); + KPDCamouflageMode = StringOptionItem.Create(19500, "KPDCamouflageMode", CamouflageMode, 0, TabGroup.SystemSettings, false) + .SetHeader(true) + .SetColor(new Color32(255, 192, 203, byte.MaxValue)); + + //DebugModeManager.SetupCustomOption(); + + EnableUpMode = BooleanOptionItem.Create(19600, "EnableYTPlan", false, TabGroup.SystemSettings, false) + .SetColor(Color.cyan) + .SetHeader(true); + + #endregion + + #region 游戏设置 + + //SoloKombat + SoloKombatManager.SetupCustomOption(); + + + //驱逐相关设定 + TextOptionItem.Create(100023, "MenuTitle.Ejections", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); + + CEMode = StringOptionItem.Create(19800, "ConfirmEjectionsMode", ConfirmEjectionsMode, 2, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); + ShowImpRemainOnEject = BooleanOptionItem.Create(19810, "ShowImpRemainOnEject", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); + ShowNKRemainOnEject = BooleanOptionItem.Create(19811, "ShowNKRemainOnEject", true, TabGroup.GameSettings, false) + .SetParent(ShowImpRemainOnEject) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); + ShowTeamNextToRoleNameOnEject = BooleanOptionItem.Create(19812, "ShowTeamNextToRoleNameOnEject", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); + ConfirmEgoistOnEject = BooleanOptionItem.Create(19813, "ConfirmEgoistOnEject", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)) + .SetHeader(true); + /* ConfirmSidekickOnEject = BooleanOptionItem.Create(19814, "ConfirmSidekickOnEject", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); */ + ConfirmLoversOnEject = BooleanOptionItem.Create(19815, "ConfirmLoversOnEject", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 238, 232, byte.MaxValue)); + + //Maps Settings + TextOptionItem.Create(100024, "MenuTitle.MapsSettings", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(19, 188, 233, byte.MaxValue)); + + // Random Maps Mode + RandomMapsMode = BooleanOptionItem.Create(19900, "RandomMapsMode", false, TabGroup.GameSettings, false) + .SetHeader(true) + .SetColor(new Color32(19, 188, 233, byte.MaxValue)); + AddedTheSkeld = BooleanOptionItem.Create(19910, "AddedTheSkeld", false, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode); + AddedMiraHQ = BooleanOptionItem.Create(19911, "AddedMIRAHQ", false, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode); + AddedPolus = BooleanOptionItem.Create(19912, "AddedPolus", false, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode); + AddedTheAirship = BooleanOptionItem.Create(19913, "AddedTheAirship", false, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode); + // MapDleks = CustomOption.Create(19914, Color.white, "AddedDleks", false, RandomMapMode); + +/* + SkeldChance = IntegerOptionItem.Create(19910, "SkeldChance", new(0, 100, 5), 0, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode) + .SetValueFormat(OptionFormat.Percent); + MiraChance = IntegerOptionItem.Create(19911, "MiraChance", new(0, 100, 5), 0, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode) + .SetValueFormat(OptionFormat.Percent); + PolusChance = IntegerOptionItem.Create(19912, "PolusChance", new(0, 100, 5), 0, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode) + .SetValueFormat(OptionFormat.Percent); + AirshipChance = IntegerOptionItem.Create(19913, "AirshipChance", new(0, 100, 5), 0, TabGroup.GameSettings, false) + .SetParent(RandomMapsMode) + .SetValueFormat(OptionFormat.Percent); + +*/ + + // Random Spawn + RandomSpawn = BooleanOptionItem.Create(22000, "RandomSpawn", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(19, 188, 233, byte.MaxValue)); + AirshipAdditionalSpawn = BooleanOptionItem.Create(22010, "AirshipAdditionalSpawn", false, TabGroup.GameSettings, false) + .SetParent(RandomSpawn) + .SetGameMode(CustomGameMode.Standard); + + // Airship Variable Electrical + AirshipVariableElectrical = BooleanOptionItem.Create(22100, "AirshipVariableElectrical", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(19, 188, 233, byte.MaxValue)); + //Disable Airship Moving Platform + DisableAirshipMovingPlatform = BooleanOptionItem.Create(22110, "DisableAirshipMovingPlatform", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(19, 188, 233, byte.MaxValue)); + + + // Sabotage + TextOptionItem.Create(100025, "MenuTitle.Sabotage", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(243, 96, 96, byte.MaxValue)) + .SetHeader(true); + + // CommsCamouflage + CommsCamouflage = BooleanOptionItem.Create(22200, "CommsCamouflage", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(243, 96, 96, byte.MaxValue)); + DisableReportWhenCC = BooleanOptionItem.Create(22300, "DisableReportWhenCC", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(243, 96, 96, byte.MaxValue)); + + // SabotageTimeControl + SabotageTimeControl = BooleanOptionItem.Create(22400, "SabotageTimeControl", false, TabGroup.GameSettings, false) + .SetColor(new Color32(243, 96, 96, byte.MaxValue)) + .SetGameMode(CustomGameMode.Standard); + PolusReactorTimeLimit = FloatOptionItem.Create(22410, "PolusReactorTimeLimit", new(1f, 60f, 1f), 30f, TabGroup.GameSettings, false) + .SetParent(SabotageTimeControl) + .SetValueFormat(OptionFormat.Seconds) + .SetGameMode(CustomGameMode.Standard); + AirshipReactorTimeLimit = FloatOptionItem.Create(22411, "AirshipReactorTimeLimit", new(1f, 90f, 1f), 60f, TabGroup.GameSettings, false) + .SetParent(SabotageTimeControl) + .SetValueFormat(OptionFormat.Seconds) + .SetGameMode(CustomGameMode.Standard); + + // LightsOutSpecialSettings + LightsOutSpecialSettings = BooleanOptionItem.Create(22500, "LightsOutSpecialSettings", false, TabGroup.GameSettings, false) + .SetColor(new Color32(243, 96, 96, byte.MaxValue)) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipViewingDeckLightsPanel = BooleanOptionItem.Create(22510, "DisableAirshipViewingDeckLightsPanel", false, TabGroup.GameSettings, false) + .SetParent(LightsOutSpecialSettings) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipGapRoomLightsPanel = BooleanOptionItem.Create(22511, "DisableAirshipGapRoomLightsPanel", false, TabGroup.GameSettings, false) + .SetParent(LightsOutSpecialSettings) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipCargoLightsPanel = BooleanOptionItem.Create(22512, "DisableAirshipCargoLightsPanel", false, TabGroup.GameSettings, false) + .SetParent(LightsOutSpecialSettings) + .SetGameMode(CustomGameMode.Standard); + + + //禁用相关设定 + TextOptionItem.Create(100026, "MenuTitle.Disable", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + + DisableVanillaRoles = BooleanOptionItem.Create(22600, "DisableVanillaRoles", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + /* DisableHiddenRoles = BooleanOptionItem.Create(22610, "DisableHiddenRoles", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + DisableSunnyboy = BooleanOptionItem.Create(22620, "DisableSunnyboy", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetParent(DisableHiddenRoles) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + DisableBard = BooleanOptionItem.Create(22630, "DisableBard", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetParent(DisableHiddenRoles) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); */ + /* DisableSaboteur = BooleanOptionItem.Create(22640, "DisableSaboteur", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetParent(DisableHiddenRoles) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); */ + DisableTaskWin = BooleanOptionItem.Create(22650, "DisableTaskWin", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + + // 禁用任务 + + DisableMeeting = BooleanOptionItem.Create(22700, "DisableMeeting", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + DisableSabotage = BooleanOptionItem.Create(22800, "DisableSabotage", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + DisableCloseDoor = BooleanOptionItem.Create(22810, "DisableCloseDoor", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetParent(DisableSabotage) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)); + + //禁用设备 + DisableDevices = BooleanOptionItem.Create(22900, "DisableDevices", false, TabGroup.GameSettings, false) + .SetColor(new Color32(255, 153, 153, byte.MaxValue)) + .SetGameMode(CustomGameMode.Standard); + DisableSkeldDevices = BooleanOptionItem.Create(22905, "DisableSkeldDevices", false, TabGroup.GameSettings, false) + .SetParent(DisableDevices) + .SetGameMode(CustomGameMode.Standard); + DisableSkeldAdmin = BooleanOptionItem.Create(22906, "DisableSkeldAdmin", false, TabGroup.GameSettings, false) + .SetParent(DisableSkeldDevices) + .SetGameMode(CustomGameMode.Standard); + DisableSkeldCamera = BooleanOptionItem.Create(22907, "DisableSkeldCamera", false, TabGroup.GameSettings, false) + .SetParent(DisableSkeldDevices) + .SetGameMode(CustomGameMode.Standard); + DisableMiraHQDevices = BooleanOptionItem.Create(22908, "DisableMiraHQDevices", false, TabGroup.GameSettings, false) + .SetParent(DisableDevices) + .SetGameMode(CustomGameMode.Standard); + DisableMiraHQAdmin = BooleanOptionItem.Create(22909, "DisableMiraHQAdmin", false, TabGroup.GameSettings, false) + .SetParent(DisableMiraHQDevices) + .SetGameMode(CustomGameMode.Standard); + DisableMiraHQDoorLog = BooleanOptionItem.Create(22910, "DisableMiraHQDoorLog", false, TabGroup.GameSettings, false) + .SetParent(DisableMiraHQDevices) + .SetGameMode(CustomGameMode.Standard); + DisablePolusDevices = BooleanOptionItem.Create(22911, "DisablePolusDevices", false, TabGroup.GameSettings, false) + .SetParent(DisableDevices) + .SetGameMode(CustomGameMode.Standard); + DisablePolusAdmin = BooleanOptionItem.Create(22912, "DisablePolusAdmin", false, TabGroup.GameSettings, false) + .SetParent(DisablePolusDevices) + .SetGameMode(CustomGameMode.Standard); + DisablePolusCamera = BooleanOptionItem.Create(22913, "DisablePolusCamera", false, TabGroup.GameSettings, false) + .SetParent(DisablePolusDevices) + .SetGameMode(CustomGameMode.Standard); + DisablePolusVital = BooleanOptionItem.Create(22914, "DisablePolusVital", false, TabGroup.GameSettings, false) + .SetParent(DisablePolusDevices) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipDevices = BooleanOptionItem.Create(22915, "DisableAirshipDevices", false, TabGroup.GameSettings, false) + .SetParent(DisableDevices) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipCockpitAdmin = BooleanOptionItem.Create(22916, "DisableAirshipCockpitAdmin", false, TabGroup.GameSettings, false) + .SetParent(DisableAirshipDevices) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipRecordsAdmin = BooleanOptionItem.Create(22917, "DisableAirshipRecordsAdmin", false, TabGroup.GameSettings, false) + .SetParent(DisableAirshipDevices) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipCamera = BooleanOptionItem.Create(22918, "DisableAirshipCamera", false, TabGroup.GameSettings, false) + .SetParent(DisableAirshipDevices) + .SetGameMode(CustomGameMode.Standard); + DisableAirshipVital = BooleanOptionItem.Create(22919, "DisableAirshipVital", false, TabGroup.GameSettings, false) + .SetParent(DisableAirshipDevices) + .SetGameMode(CustomGameMode.Standard); + DisableDevicesIgnoreConditions = BooleanOptionItem.Create(22920, "IgnoreConditions", false, TabGroup.GameSettings, false) + .SetParent(DisableDevices) + .SetGameMode(CustomGameMode.Standard); + DisableDevicesIgnoreImpostors = BooleanOptionItem.Create(22921, "IgnoreImpostors", false, TabGroup.GameSettings, false) + .SetParent(DisableDevicesIgnoreConditions) + .SetGameMode(CustomGameMode.Standard); + DisableDevicesIgnoreNeutrals = BooleanOptionItem.Create(22922, "IgnoreNeutrals", false, TabGroup.GameSettings, false) + .SetParent(DisableDevicesIgnoreConditions) + .SetGameMode(CustomGameMode.Standard); + DisableDevicesIgnoreCrewmates = BooleanOptionItem.Create(22923, "IgnoreCrewmates", false, TabGroup.GameSettings, false) + .SetParent(DisableDevicesIgnoreConditions) + .SetGameMode(CustomGameMode.Standard); + DisableDevicesIgnoreAfterAnyoneDied = BooleanOptionItem.Create(22924, "IgnoreAfterAnyoneDied", false, TabGroup.GameSettings, false) + .SetParent(DisableDevicesIgnoreConditions) + .SetGameMode(CustomGameMode.Standard); + + //Disable Short Tasks + DisableShortTasks = BooleanOptionItem.Create(23000, "DisableShortTasks", false, TabGroup.TaskSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(239, 89, 175, byte.MaxValue)); + DisableCleanVent = BooleanOptionItem.Create(23001, "DisableCleanVent", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableCalibrateDistributor = BooleanOptionItem.Create(23002, "DisableCalibrateDistributor", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableChartCourse = BooleanOptionItem.Create(23003, "DisableChartCourse", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableStabilizeSteering = BooleanOptionItem.Create(23004, "DisableStabilizeSteering", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableCleanO2Filter = BooleanOptionItem.Create(23005, "DisableCleanO2Filter", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableUnlockManifolds = BooleanOptionItem.Create(23006, "DisableUnlockManifolds", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisablePrimeShields = BooleanOptionItem.Create(23007, "DisablePrimeShields", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableMeasureWeather = BooleanOptionItem.Create(23008, "DisableMeasureWeather", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableBuyBeverage = BooleanOptionItem.Create(23009, "DisableBuyBeverage", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableAssembleArtifact = BooleanOptionItem.Create(23010, "DisableAssembleArtifact", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableSortSamples = BooleanOptionItem.Create(23011, "DisableSortSamples", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableProcessData = BooleanOptionItem.Create(23012, "DisableProcessData", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableRunDiagnostics = BooleanOptionItem.Create(23013, "DisableRunDiagnostics", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableRepairDrill = BooleanOptionItem.Create(23014, "DisableRepairDrill", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableAlignTelescope = BooleanOptionItem.Create(23015, "DisableAlignTelescope", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableRecordTemperature = BooleanOptionItem.Create(23016, "DisableRecordTemperature", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableFillCanisters = BooleanOptionItem.Create(23017, "DisableFillCanisters", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableMonitorTree = BooleanOptionItem.Create(23018, "DisableMonitorTree", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableStoreArtifacts = BooleanOptionItem.Create(23019, "DisableStoreArtifacts", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisablePutAwayPistols = BooleanOptionItem.Create(23020, "DisablePutAwayPistols", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisablePutAwayRifles = BooleanOptionItem.Create(23021, "DisablePutAwayRifles", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableMakeBurger = BooleanOptionItem.Create(23022, "DisableMakeBurger", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableCleanToilet = BooleanOptionItem.Create(23023, "DisableCleanToilet", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableDecontaminate = BooleanOptionItem.Create(23024, "DisableDecontaminate", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableSortRecords = BooleanOptionItem.Create(23025, "DisableSortRecords", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableFixShower = BooleanOptionItem.Create(23026, "DisableFixShower", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisablePickUpTowels = BooleanOptionItem.Create(23027, "DisablePickUpTowels", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisablePolishRuby = BooleanOptionItem.Create(23028, "DisablePolishRuby", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + DisableDressMannequin = BooleanOptionItem.Create(23029, "DisableDressMannequin", false, TabGroup.TaskSettings, false) + .SetParent(DisableShortTasks) + .SetGameMode(CustomGameMode.Standard); + + //Disable Common Tasks + DisableCommonTasks = BooleanOptionItem.Create(23100, "DisableCommonTasks", false, TabGroup.TaskSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(239, 89, 175, byte.MaxValue)); + DisableSwipeCard = BooleanOptionItem.Create(23101, "DisableSwipeCardTask", false, TabGroup.TaskSettings, false) + .SetParent(DisableCommonTasks) + .SetGameMode(CustomGameMode.Standard); + DisableFixWiring = BooleanOptionItem.Create(23102, "DisableFixWiring", false, TabGroup.TaskSettings, false) + .SetParent(DisableCommonTasks) + .SetGameMode(CustomGameMode.Standard); + DisableEnterIdCode = BooleanOptionItem.Create(23103, "DisableEnterIdCode", false, TabGroup.TaskSettings, false) + .SetParent(DisableCommonTasks) + .SetGameMode(CustomGameMode.Standard); + DisableInsertKeys = BooleanOptionItem.Create(23104, "DisableInsertKeys", false, TabGroup.TaskSettings, false) + .SetParent(DisableCommonTasks) + .SetGameMode(CustomGameMode.Standard); + DisableScanBoardingPass = BooleanOptionItem.Create(23105, "DisableScanBoardingPass", false, TabGroup.TaskSettings, false) + .SetParent(DisableCommonTasks) + .SetGameMode(CustomGameMode.Standard); + + //Disable Long Tasks + DisableLongTasks = BooleanOptionItem.Create(23150, "DisableLongTasks", false, TabGroup.TaskSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(239, 89, 175, byte.MaxValue)); + DisableSubmitScan = BooleanOptionItem.Create(23151, "DisableSubmitScanTask", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableUnlockSafe = BooleanOptionItem.Create(23152, "DisableUnlockSafeTask", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableStartReactor = BooleanOptionItem.Create(23153, "DisableStartReactorTask", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableResetBreaker = BooleanOptionItem.Create(23154, "DisableResetBreakerTask", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableAlignEngineOutput = BooleanOptionItem.Create(23155, "DisableAlignEngineOutput", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableInspectSample = BooleanOptionItem.Create(23156, "DisableInspectSample", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableEmptyChute = BooleanOptionItem.Create(23157, "DisableEmptyChute", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableClearAsteroids = BooleanOptionItem.Create(23158, "DisableClearAsteroids", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableWaterPlants = BooleanOptionItem.Create(23159, "DisableWaterPlants", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableOpenWaterways = BooleanOptionItem.Create(23160, "DisableOpenWaterways", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableReplaceWaterJug = BooleanOptionItem.Create(23161, "DisableReplaceWaterJug", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableRebootWifi = BooleanOptionItem.Create(23162, "DisableRebootWifi", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableDevelopPhotos = BooleanOptionItem.Create(23163, "DisableDevelopPhotos", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableRewindTapes = BooleanOptionItem.Create(23164, "DisableRewindTapes", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + DisableStartFans = BooleanOptionItem.Create(23165, "DisableStartFans", false, TabGroup.TaskSettings, false) + .SetParent(DisableLongTasks) + .SetGameMode(CustomGameMode.Standard); + + //Disable Divert Power, Weather Nodes and etc. situational Tasks + DisableOtherTasks = BooleanOptionItem.Create(23200, "DisableOtherTasks", false, TabGroup.TaskSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(239, 89, 175, byte.MaxValue)); + DisableUploadData = BooleanOptionItem.Create(23205, "DisableUploadDataTask", false, TabGroup.TaskSettings, false) + .SetParent(DisableOtherTasks) + .SetGameMode(CustomGameMode.Standard); + DisableEmptyGarbage = BooleanOptionItem.Create(23206, "DisableEmptyGarbage", false, TabGroup.TaskSettings, false) + .SetParent(DisableOtherTasks) + .SetGameMode(CustomGameMode.Standard); + DisableFuelEngines = BooleanOptionItem.Create(23207, "DisableFuelEngines", false, TabGroup.TaskSettings, false) + .SetParent(DisableOtherTasks) + .SetGameMode(CustomGameMode.Standard); + DisableDivertPower = BooleanOptionItem.Create(23208, "DisableDivertPower", false, TabGroup.TaskSettings, false) + .SetParent(DisableOtherTasks) + .SetGameMode(CustomGameMode.Standard); + DisableActivateWeatherNodes = BooleanOptionItem.Create(23209, "DisableActivateWeatherNodes", false, TabGroup.TaskSettings, false) + .SetParent(DisableOtherTasks) + .SetGameMode(CustomGameMode.Standard); + + TextOptionItem.Create(100022, "MenuTitle.Guessers", TabGroup.TaskSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(Color.yellow) + .SetHeader(true); + GuesserMode = BooleanOptionItem.Create(19700, "GuesserMode", false, TabGroup.TaskSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(Color.yellow) + .SetHeader(true); + CrewmatesCanGuess = BooleanOptionItem.Create(19710, "CrewmatesCanGuess", false, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + ImpostorsCanGuess = BooleanOptionItem.Create(19711, "ImpostorsCanGuess", false, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + NeutralKillersCanGuess = BooleanOptionItem.Create(19712, "NeutralKillersCanGuess", false, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + PassiveNeutralsCanGuess = BooleanOptionItem.Create(19713, "PassiveNeutralsCanGuess", false, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + CanGuessAddons = BooleanOptionItem.Create(19714, "CanGuessAddons", true, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + CrewCanGuessCrew = BooleanOptionItem.Create(19715, "CrewCanGuessCrew", true, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + ImpCanGuessImp = BooleanOptionItem.Create(19716, "ImpCanGuessImp", true, TabGroup.TaskSettings, false) + .SetParent(GuesserMode); + HideGuesserCommands = BooleanOptionItem.Create(19717, "GuesserTryHideMsg", true, TabGroup.TaskSettings, false) + .SetParent(GuesserMode) + .SetColor(Color.green); + + TextOptionItem.Create(100050, "MenuTitle.GuesserModeRoles", TabGroup.TaskSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(Color.yellow) + .SetHeader(true); + SetupAdtRoleOptions(14500, CustomRoles.Onbound, canSetNum: true, tab: TabGroup.TaskSettings); + ImpCanBeOnbound = BooleanOptionItem.Create(14510, "ImpCanBeOnbound", true, TabGroup.TaskSettings, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Onbound]); + CrewCanBeOnbound = BooleanOptionItem.Create(14511, "CrewCanBeOnbound", true, TabGroup.TaskSettings, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Onbound]); + NeutralCanBeOnbound = BooleanOptionItem.Create(14512, "NeutralCanBeOnbound", true, TabGroup.TaskSettings, false) + .SetParent(CustomRoleSpawnChances[CustomRoles.Onbound]); + + + + //会议相关设定 + TextOptionItem.Create(100027, "MenuTitle.Meeting", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(147, 241, 240, byte.MaxValue)); + + // 会议限制次数 + SyncButtonMode = BooleanOptionItem.Create(23300, "SyncButtonMode", false, TabGroup.GameSettings, false) + .SetHeader(true) + .SetColor(new Color32(147, 241, 240, byte.MaxValue)) + .SetGameMode(CustomGameMode.Standard); + SyncedButtonCount = IntegerOptionItem.Create(23310, "SyncedButtonCount", new(0, 100, 1), 10, TabGroup.GameSettings, false) + .SetParent(SyncButtonMode) + .SetValueFormat(OptionFormat.Times) + .SetGameMode(CustomGameMode.Standard); + + // 全员存活时的会议时间 + AllAliveMeeting = BooleanOptionItem.Create(23400, "AllAliveMeeting", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(147, 241, 240, byte.MaxValue)); + AllAliveMeetingTime = FloatOptionItem.Create(23410, "AllAliveMeetingTime", new(1f, 300f, 1f), 10f, TabGroup.GameSettings, false) + .SetParent(AllAliveMeeting) + .SetValueFormat(OptionFormat.Seconds); + + // 附加紧急会议 + AdditionalEmergencyCooldown = BooleanOptionItem.Create(23500, "AdditionalEmergencyCooldown", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(147, 241, 240, byte.MaxValue)); + AdditionalEmergencyCooldownThreshold = IntegerOptionItem.Create(23510, "AdditionalEmergencyCooldownThreshold", new(1, 15, 1), 1, TabGroup.GameSettings, false) + .SetParent(AdditionalEmergencyCooldown) + .SetGameMode(CustomGameMode.Standard) + .SetValueFormat(OptionFormat.Players); + AdditionalEmergencyCooldownTime = FloatOptionItem.Create(23511, "AdditionalEmergencyCooldownTime", new(1f, 60f, 1f), 1f, TabGroup.GameSettings, false) + .SetParent(AdditionalEmergencyCooldown) + .SetGameMode(CustomGameMode.Standard) + .SetValueFormat(OptionFormat.Seconds); + + // 投票相关设定 + VoteMode = BooleanOptionItem.Create(23600, "VoteMode", false, TabGroup.GameSettings, false) + .SetColor(new Color32(147, 241, 240, byte.MaxValue)) + .SetGameMode(CustomGameMode.Standard); + WhenSkipVote = StringOptionItem.Create(23610, "WhenSkipVote", voteModes[0..3], 0, TabGroup.GameSettings, false) + .SetParent(VoteMode) + .SetGameMode(CustomGameMode.Standard); + WhenSkipVoteIgnoreFirstMeeting = BooleanOptionItem.Create(23611, "WhenSkipVoteIgnoreFirstMeeting", false, TabGroup.GameSettings, false) + .SetParent(WhenSkipVote) + .SetGameMode(CustomGameMode.Standard); + WhenSkipVoteIgnoreNoDeadBody = BooleanOptionItem.Create(23612, "WhenSkipVoteIgnoreNoDeadBody", false, TabGroup.GameSettings, false) + .SetParent(WhenSkipVote) + .SetGameMode(CustomGameMode.Standard); + WhenSkipVoteIgnoreEmergency = BooleanOptionItem.Create(23613, "WhenSkipVoteIgnoreEmergency", false, TabGroup.GameSettings, false) + .SetParent(WhenSkipVote) + .SetGameMode(CustomGameMode.Standard); + WhenNonVote = StringOptionItem.Create(23700, "WhenNonVote", voteModes, 0, TabGroup.GameSettings, false) + .SetParent(VoteMode) + .SetGameMode(CustomGameMode.Standard); + WhenTie = StringOptionItem.Create(23750, "WhenTie", tieModes, 0, TabGroup.GameSettings, false) + .SetParent(VoteMode) + .SetGameMode(CustomGameMode.Standard); + + + // 其它设定 + TextOptionItem.Create(100028, "MenuTitle.Other", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)); + + // 梯子摔死 + LadderDeath = BooleanOptionItem.Create(23800, "LadderDeath", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)); + LadderDeathChance = StringOptionItem.Create(23810, "LadderDeathChance", rates[1..], 0, TabGroup.GameSettings, false) + .SetParent(LadderDeath) + .SetGameMode(CustomGameMode.Standard); + + // 修正首刀时间 + FixFirstKillCooldown = BooleanOptionItem.Create(23900, "FixFirstKillCooldown", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)); + + // 首刀保护 + ShieldPersonDiedFirst = BooleanOptionItem.Create(24000, "ShieldPersonDiedFirst", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)); + + // 杀戮闪烁持续 + KillFlashDuration = FloatOptionItem.Create(24100, "KillFlashDuration", new(0.1f, 0.45f, 0.05f), 0.3f, TabGroup.GameSettings, false) + .SetColor(new Color32(193, 255, 209, byte.MaxValue)) + .SetValueFormat(OptionFormat.Seconds) + .SetGameMode(CustomGameMode.Standard); + + // 幽灵相关设定 + TextOptionItem.Create(100029, "MenuTitle.Ghost", TabGroup.GameSettings) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(217, 218, 255, byte.MaxValue)); + + // 幽灵设置 + GhostIgnoreTasks = BooleanOptionItem.Create(24200, "GhostIgnoreTasks", false, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetHeader(true) + .SetColor(new Color32(217, 218, 255, byte.MaxValue)); + GhostCanSeeOtherRoles = BooleanOptionItem.Create(24300, "GhostCanSeeOtherRoles", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(217, 218, 255, byte.MaxValue)); + GhostCanSeeOtherVotes = BooleanOptionItem.Create(24400, "GhostCanSeeOtherVotes", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(217, 218, 255, byte.MaxValue)); + GhostCanSeeDeathReason = BooleanOptionItem.Create(24500, "GhostCanSeeDeathReason", true, TabGroup.GameSettings, false) + .SetGameMode(CustomGameMode.Standard) + .SetColor(new Color32(217, 218, 255, byte.MaxValue)); + + #endregion + + IsLoaded = true; + } + + public static void SetupRoleOptions(int id, TabGroup tab, CustomRoles role, CustomGameMode customGameMode = CustomGameMode.Standard, bool zeroOne = false) + { + var spawnOption = StringOptionItem.Create(id, role.ToString(), zeroOne ? ratesZeroOne : ratesToggle, 0, tab, false).SetColor(Utils.GetRoleColor(role)) + .SetHeader(true) + .SetGameMode(customGameMode) as StringOptionItem; + var countOption = IntegerOptionItem.Create(id + 1, "Maximum", new(1, 15, 1), 1, tab, false) + .SetParent(spawnOption) + .SetValueFormat(OptionFormat.Players) + .SetGameMode(customGameMode); + + CustomRoleSpawnChances.Add(role, spawnOption); + CustomRoleCounts.Add(role, countOption); + } + private static void SetupLoversRoleOptionsToggle(int id, CustomGameMode customGameMode = CustomGameMode.Standard) + { + var role = CustomRoles.Lovers; + var spawnOption = StringOptionItem.Create(id, role.ToString(), ratesZeroOne, 0, TabGroup.Addons, false).SetColor(Utils.GetRoleColor(role)) + .SetHeader(true) + .SetGameMode(customGameMode) as StringOptionItem; + + LoverSpawnChances = IntegerOptionItem.Create(id + 2, "LoverSpawnChances", new(0, 100, 5), 50, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetValueFormat(OptionFormat.Percent) + .SetGameMode(customGameMode); + + LoverKnowRoles = BooleanOptionItem.Create(id + 4, "LoverKnowRoles", true, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetGameMode(customGameMode); + + LoverSuicide = BooleanOptionItem.Create(id + 3, "LoverSuicide", true, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetGameMode(customGameMode); + + ImpCanBeInLove = BooleanOptionItem.Create(id + 5, "ImpCanBeInLove", true, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetGameMode(customGameMode); + + CrewCanBeInLove = BooleanOptionItem.Create(id + 6, "CrewCanBeInLove", true, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetGameMode(customGameMode); + + NeutralCanBeInLove = BooleanOptionItem.Create(id + 7, "NeutralCanBeInLove", true, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetGameMode(customGameMode); + + + var countOption = IntegerOptionItem.Create(id + 1, "NumberOfLovers", new(2, 2, 1), 2, TabGroup.Addons, false) + .SetParent(spawnOption) + .SetHidden(true) + .SetGameMode(customGameMode); + + CustomRoleSpawnChances.Add(role, spawnOption); + CustomRoleCounts.Add(role, countOption); + } + + private static void SetupAdtRoleOptions(int id, CustomRoles role, CustomGameMode customGameMode = CustomGameMode.Standard, bool canSetNum = false, TabGroup tab = TabGroup.Addons, bool canSetChance = true) + { + var spawnOption = StringOptionItem.Create(id, role.ToString(), ratesZeroOne, 0, tab, false).SetColor(Utils.GetRoleColor(role)) + .SetHeader(true) + .SetGameMode(customGameMode) as StringOptionItem; + + var countOption = IntegerOptionItem.Create(id + 1, "Maximum", new(1, canSetNum ? 10 : 1, 1), 1, tab, false) + .SetParent(spawnOption) + .SetValueFormat(OptionFormat.Players) + .SetHidden(!canSetNum) + .SetGameMode(customGameMode); + + var spawnRateOption = IntegerOptionItem.Create(id + 2, "AdditionRolesSpawnRate", new(0, 100, 5), canSetChance ? 65 : 100, tab, false) + .SetParent(spawnOption) + .SetValueFormat(OptionFormat.Percent) + .SetHidden(!canSetChance) + .SetGameMode(customGameMode) as IntegerOptionItem; + + CustomAdtRoleSpawnRate.Add(role, spawnRateOption); + CustomRoleSpawnChances.Add(role, spawnOption); + CustomRoleCounts.Add(role, countOption); + } + + public static void SetupSingleRoleOptions(int id, TabGroup tab, CustomRoles role, int count, CustomGameMode customGameMode = CustomGameMode.Standard, bool zeroOne = false) + { + var spawnOption = StringOptionItem.Create(id, role.ToString(), zeroOne ? ratesZeroOne : ratesToggle, 0, tab, false).SetColor(Utils.GetRoleColor(role)) + .SetHeader(true) + .SetGameMode(customGameMode) as StringOptionItem; + // 初期値,最大値,最小値が同じで、stepが0のどうやっても変えることができない個数オプション + var countOption = IntegerOptionItem.Create(id + 1, "Maximum", new(count, count, count), count, tab, false) + .SetParent(spawnOption) + .SetGameMode(customGameMode); + + CustomRoleSpawnChances.Add(role, spawnOption); + CustomRoleCounts.Add(role, countOption); + } + public class OverrideTasksData + { + public static Dictionary AllData = new(); + public CustomRoles Role { get; private set; } + public int IdStart { get; private set; } + public OptionItem doOverride; + public OptionItem assignCommonTasks; + public OptionItem numLongTasks; + public OptionItem numShortTasks; + + public OverrideTasksData(int idStart, TabGroup tab, CustomRoles role) + { + IdStart = idStart; + Role = role; + Dictionary replacementDic = new() { { "%role%", Utils.ColorString(Utils.GetRoleColor(role), Utils.GetRoleName(role)) } }; + doOverride = BooleanOptionItem.Create(idStart++, "doOverride", false, tab, false) + .SetParent(CustomRoleSpawnChances[role]) + .SetValueFormat(OptionFormat.None); + doOverride.ReplacementDictionary = replacementDic; + assignCommonTasks = BooleanOptionItem.Create(idStart++, "assignCommonTasks", true, tab, false) + .SetParent(doOverride) + .SetValueFormat(OptionFormat.None); + assignCommonTasks.ReplacementDictionary = replacementDic; + numLongTasks = IntegerOptionItem.Create(idStart++, "roleLongTasksNum", new(0, 99, 1), 3, tab, false) + .SetParent(doOverride) + .SetValueFormat(OptionFormat.Pieces); + numLongTasks.ReplacementDictionary = replacementDic; + numShortTasks = IntegerOptionItem.Create(idStart++, "roleShortTasksNum", new(0, 99, 1), 3, tab, false) + .SetParent(doOverride) + .SetValueFormat(OptionFormat.Pieces); + numShortTasks.ReplacementDictionary = replacementDic; + + if (!AllData.ContainsKey(role)) AllData.Add(role, this); + else Logger.Warn("重複したCustomRolesを対象とするOverrideTasksDataが作成されました", "OverrideTasksData"); + } + public static OverrideTasksData Create(int idStart, TabGroup tab, CustomRoles role) + { + return new OverrideTasksData(idStart, tab, role); + } + } +} diff --git a/Modules/OptionItem/BooleanOptionItem.cs b/Modules/OptionItem/BooleanOptionItem.cs new file mode 100644 index 000000000..d66c568e9 --- /dev/null +++ b/Modules/OptionItem/BooleanOptionItem.cs @@ -0,0 +1,37 @@ +namespace TOHE; + +public class BooleanOptionItem : OptionItem +{ + public const string TEXT_true = "ColoredOn"; + public const string TEXT_false = "ColoredOff"; + + // コンストラクタ + public BooleanOptionItem(int id, string name, bool defaultValue, TabGroup tab, bool isSingleValue) + : base(id, name, defaultValue ? 1 : 0, tab, isSingleValue) + { + } + public static BooleanOptionItem Create( + int id, string name, bool defaultValue, TabGroup tab, bool isSingleValue + ) + { + return new BooleanOptionItem( + id, name, defaultValue, tab, isSingleValue + ); + } + + // Getter + public override string GetString() + { + return Translator.GetString(GetBool() ? TEXT_true : TEXT_false); + } + + // Setter + public override void SetValue(int value) + { + base.SetValue(value % 2 == 0 ? 0 : 1); + } + public override void SetValueNoRpc(int value) + { + base.SetValueNoRpc(value % 2 == 0 ? 0 : 1); + } +} \ No newline at end of file diff --git a/Modules/OptionItem/FloatOptionItem.cs b/Modules/OptionItem/FloatOptionItem.cs new file mode 100644 index 000000000..b06131aab --- /dev/null +++ b/Modules/OptionItem/FloatOptionItem.cs @@ -0,0 +1,42 @@ +namespace TOHE; + +public class FloatOptionItem : OptionItem +{ + // 必須情報 + public FloatValueRule Rule; + + // コンストラクタ + public FloatOptionItem(int id, string name, float defaultValue, TabGroup tab, bool isSingleValue, FloatValueRule rule) + : base(id, name, rule.GetNearestIndex(defaultValue), tab, isSingleValue) + { + Rule = rule; + } + public static FloatOptionItem Create( + int id, string name, FloatValueRule rule, float defaultValue, TabGroup tab, bool isSingleValue + ) + { + return new FloatOptionItem( + id, name, defaultValue, tab, isSingleValue, rule + ); + } + + // Getter + public override int GetInt() => (int)Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return ApplyFormat(((float)((int)(Rule.GetValueByIndex(CurrentValue) * 100) * 1.0) / 100).ToString()); + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + // Setter + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + } + public override void SetValueNoRpc(int value) + { + base.SetValueNoRpc(Rule.RepeatIndex(value)); + } +} \ No newline at end of file diff --git a/Modules/OptionItem/IntegerOptionItem.cs b/Modules/OptionItem/IntegerOptionItem.cs new file mode 100644 index 000000000..4fc3beefd --- /dev/null +++ b/Modules/OptionItem/IntegerOptionItem.cs @@ -0,0 +1,42 @@ +namespace TOHE; + +public class IntegerOptionItem : OptionItem +{ + // 必須情報 + public IntegerValueRule Rule; + + // コンストラクタ + public IntegerOptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue, IntegerValueRule rule) + : base(id, name, rule.GetNearestIndex(defaultValue), tab, isSingleValue) + { + Rule = rule; + } + public static IntegerOptionItem Create( + int id, string name, IntegerValueRule rule, int defaultValue, TabGroup tab, bool isSingleValue + ) + { + return new IntegerOptionItem( + id, name, defaultValue, tab, isSingleValue, rule + ); + } + + // Getter + public override int GetInt() => Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return ApplyFormat(Rule.GetValueByIndex(CurrentValue).ToString()); + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + // Setter + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + } + public override void SetValueNoRpc(int value) + { + base.SetValueNoRpc(Rule.RepeatIndex(value)); + } +} \ No newline at end of file diff --git a/Modules/OptionItem/OptionItem.cs b/Modules/OptionItem/OptionItem.cs new file mode 100644 index 000000000..548d5dac6 --- /dev/null +++ b/Modules/OptionItem/OptionItem.cs @@ -0,0 +1,289 @@ +using BepInEx.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace TOHE; + +public abstract class OptionItem +{ + #region static + public static IReadOnlyList AllOptions => _allOptions; + private static List _allOptions = new(); + public static int CurrentPreset { get; set; } + #endregion + + // 必須情報 (コンストラクタで必ず設定させる必要がある値) + public int Id { get; } + public string Name { get; } + public int DefaultValue { get; } + public TabGroup Tab { get; } + public bool IsSingleValue { get; } + + // 任意情報 (空・nullを許容する または、ほとんど初期値で問題ない値) + public Color NameColor { get; protected set; } + public OptionFormat ValueFormat { get; protected set; } + public CustomGameMode GameMode { get; protected set; } + public bool IsHeader { get; protected set; } + public bool IsHidden { get; protected set; } + public bool IsText { get; protected set; } + public Dictionary ReplacementDictionary + { + get => _replacementDictionary; + set + { + if (value == null) _replacementDictionary?.Clear(); + else _replacementDictionary = value; + } + } + private Dictionary _replacementDictionary; + + // 設定値情報 (オプションの値に関わる情報) + public int CurrentValue + { + get => GetValue(); + set => SetValue(value); + } + + // 親子情報 + public OptionItem Parent { get; private set; } + public List Children; + + // 内部情報 (外部から参照することを想定していない情報) + public ConfigEntry CurrentEntry => + IsSingleValue ? singleEntry : AllConfigEntries[CurrentPreset]; + private ConfigEntry[] AllConfigEntries; + private ConfigEntry singleEntry; + + + public OptionBehaviour OptionBehaviour; + + // イベント + // eventキーワードにより、クラス外からのこのフィールドに対する以下の操作は禁止されます。 + // - 代入 (+=, -=を除く) + // - 直接的な呼び出し + public event EventHandler UpdateValueEvent; + + // コンストラクタ + public OptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue) + { + // 必須情報の設定 + Id = id; + Name = name; + DefaultValue = defaultValue; + Tab = tab; + IsSingleValue = isSingleValue; + + // 任意情報の初期値設定 + NameColor = Color.white; + ValueFormat = OptionFormat.None; + GameMode = CustomGameMode.All; + IsHeader = false; + IsHidden = false; + IsText = false; + + // オブジェクト初期化 + Children = new(); + + // ConfigEntry初期化 + AllConfigEntries = new ConfigEntry[5]; + if (Id == 0) + { + singleEntry = Main.Instance.Config.Bind("Current Preset", id.ToString(), DefaultValue); + CurrentPreset = singleEntry.Value; + } + else if (IsSingleValue) + { + singleEntry = Main.Instance.Config.Bind("SingleEntryOptions", id.ToString(), DefaultValue); + } + else + { + for (int i = 0; i < 5; i++) + { + AllConfigEntries[i] = Main.Instance.Config.Bind($"Preset{i + 1}", id.ToString(), DefaultValue); + } + } + + if (AllOptions.Any(op => op.Id == id)) + { + Logger.Error($"Repeat option ID:{id}", "OptionItem"); + } + _allOptions.Add(this); + } + + // Setter + public OptionItem Do(Action action) + { + action(this); + return this; + } + + public OptionItem SetColor(Color value) => Do(i => i.NameColor = value); + public OptionItem SetValueFormat(OptionFormat value) => Do(i => i.ValueFormat = value); + public OptionItem SetGameMode(CustomGameMode value) => Do(i => i.GameMode = value); + public OptionItem SetHeader(bool value) => Do(i => i.IsHeader = value); + public OptionItem SetHidden(bool value) => Do(i => i.IsHidden = value); + public OptionItem SetText(bool value) => Do(i => i.IsText = value); + + public OptionItem SetParent(OptionItem parent) => Do(i => + { + foreach (var role in Options.CustomRoleSpawnChances.Where(x => x.Value.Name == parent.Name)) + { + var roleName = Translator.GetString(Enum.GetName(typeof(CustomRoles), role.Key)); + ReplacementDictionary ??= new(); + ReplacementDictionary.TryAdd(roleName, Utils.ColorString(Utils.GetRoleColor(role.Key), roleName)); + break; + } + i.Parent = parent; + parent.SetChild(i); + }); + public OptionItem SetChild(OptionItem child) => Do(i => i.Children.Add(child)); + public OptionItem RegisterUpdateValueEvent(EventHandler handler) + => Do(i => UpdateValueEvent += handler); + + // 置き換え辞書 + public OptionItem AddReplacement((string key, string value) kvp) + => Do(i => + { + ReplacementDictionary ??= new(); + ReplacementDictionary.Add(kvp.key, kvp.value); + }); + public OptionItem RemoveReplacement(string key) + => Do(i => ReplacementDictionary?.Remove(key)); + + // Getter + public virtual string GetName(bool disableColor = false, bool console = false) + { + return disableColor ? + Translator.GetString(Name, ReplacementDictionary, console) : + Utils.ColorString(NameColor, Translator.GetString(Name, ReplacementDictionary)); + } + public virtual bool GetBool() => CurrentValue != 0 && (Parent == null || Parent.GetBool()); + public virtual int GetInt() => CurrentValue; + public virtual float GetFloat() => CurrentValue; + public virtual string GetString() + { + return ApplyFormat(CurrentValue.ToString()); + } + public virtual int GetValue() => CurrentEntry.Value; + + // 旧IsHidden関数 + public virtual bool IsHiddenOn(CustomGameMode mode) + { + return IsHidden || (GameMode != CustomGameMode.All && GameMode != mode); + } + + public string ApplyFormat(string value) + { + if (ValueFormat == OptionFormat.None) return value; + return string.Format(Translator.GetString("Format." + ValueFormat), value); + } + + // 外部からの操作 + public virtual void Refresh() + { + if (OptionBehaviour is not null and StringOption opt) + { + opt.TitleText.text = GetName(); + opt.ValueText.text = GetString(); + opt.oldValue = opt.Value = CurrentValue; + } + } + public virtual void SetValue(int value) + { + int beforeValue = CurrentEntry.Value; + int afterValue = CurrentEntry.Value = value; + + CallUpdateValueEvent(beforeValue, afterValue); + Refresh(); + RPC.SyncCustomSettingsRPCforOneOption(this); + } + public virtual void SetValueNoRpc(int value) + { + int beforeValue = CurrentEntry.Value; + int afterValue = CurrentEntry.Value = value; + + CallUpdateValueEvent(beforeValue, afterValue); + Refresh(); + } + + // 演算子オーバーロード + public static OptionItem operator ++(OptionItem item) + => item.Do(item => item.SetValue(item.CurrentValue + 1)); + public static OptionItem operator --(OptionItem item) + => item.Do(item => item.SetValue(item.CurrentValue - 1)); + + // 全体操作用 + public static void SwitchPreset(int newPreset) + { + CurrentPreset = Math.Clamp(newPreset, 0, 4); + + foreach (var op in AllOptions) + op.Refresh(); + + SyncAllOptions(); + } + public static void SyncAllOptions(int targetId = -1) + { + if ( + Main.AllPlayerControls.Count() <= 1 || + AmongUsClient.Instance.AmHost == false || + PlayerControl.LocalPlayer == null + ) return; + + RPC.SyncCustomSettingsRPC(targetId); + } + + + // EventArgs + private void CallUpdateValueEvent(int beforeValue, int currentValue) + { + if (UpdateValueEvent == null) return; + try + { + UpdateValueEvent(this, new UpdateValueEventArgs(beforeValue, currentValue)); + } + catch (Exception ex) + { + Logger.Error($"[{Name}] UpdateValueEventの呼び出し時に例外が発生しました", "OptionItem.UpdateValueEvent"); + Logger.Exception(ex, "OptionItem.UpdateValueEvent"); + } + } + + public class UpdateValueEventArgs : EventArgs + { + public int CurrentValue { get; set; } + public int BeforeValue { get; set; } + public UpdateValueEventArgs(int beforeValue, int currentValue) + { + CurrentValue = currentValue; + BeforeValue = beforeValue; + } + } +} + +public enum TabGroup +{ + SystemSettings, + GameSettings, + TaskSettings, + ImpostorRoles, + CrewmateRoles, + NeutralRoles, + Addons, + OtherRoles +} +public enum OptionFormat +{ + None, + Players, + Seconds, + Percent, + Times, + Multiplier, + Votes, + Pieces, + Health, + Level, +} \ No newline at end of file diff --git a/Modules/OptionItem/PresetOptionItem.cs b/Modules/OptionItem/PresetOptionItem.cs new file mode 100644 index 000000000..7cfb309ef --- /dev/null +++ b/Modules/OptionItem/PresetOptionItem.cs @@ -0,0 +1,43 @@ +namespace TOHE; + +public class PresetOptionItem : OptionItem +{ + // 必須情報 + public IntegerValueRule Rule; + + // コンストラクタ + public PresetOptionItem(int defaultValue, TabGroup tab) + : base(0, "Preset", defaultValue, tab, true) + { + Rule = (0, 4, 1); + } + public static PresetOptionItem Create(int defaultValue, TabGroup tab) + { + return new PresetOptionItem(defaultValue, tab); + } + + // Getter + public override int GetInt() => Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return CurrentValue switch + { + 0 => Main.Preset1.Value == (string)Main.Preset1.DefaultValue ? Translator.GetString("Preset_1") : Main.Preset1.Value, + 1 => Main.Preset2.Value == (string)Main.Preset2.DefaultValue ? Translator.GetString("Preset_2") : Main.Preset2.Value, + 2 => Main.Preset3.Value == (string)Main.Preset3.DefaultValue ? Translator.GetString("Preset_3") : Main.Preset3.Value, + 3 => Main.Preset4.Value == (string)Main.Preset4.DefaultValue ? Translator.GetString("Preset_4") : Main.Preset4.Value, + 4 => Main.Preset5.Value == (string)Main.Preset5.DefaultValue ? Translator.GetString("Preset_5") : Main.Preset5.Value, + _ => null, + }; + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + // Setter + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + SwitchPreset(Rule.RepeatIndex(value)); + } +} \ No newline at end of file diff --git a/Modules/OptionItem/StringOptionItem.cs b/Modules/OptionItem/StringOptionItem.cs new file mode 100644 index 000000000..22f4aec9f --- /dev/null +++ b/Modules/OptionItem/StringOptionItem.cs @@ -0,0 +1,58 @@ +namespace TOHE; + +public class StringOptionItem : OptionItem +{ + // 必須情報 + public IntegerValueRule Rule; + public string[] Selections; + + // コンストラクタ + public StringOptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue, string[] selections) + : base(id, name, defaultValue, tab, isSingleValue) + { + Rule = (0, selections.Length - 1, 1); + Selections = selections; + } + public static StringOptionItem Create( + int id, string name, string[] selections, int defaultIndex, TabGroup tab, bool isSingleValue + ) + { + return new StringOptionItem( + id, name, defaultIndex, tab, isSingleValue, selections + ); + } + + // Getter + public override int GetInt() => Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return Translator.GetString(Selections[Rule.GetValueByIndex(CurrentValue)]); + } + public int GetChance() + { + //0%or100%の場合 + if (Selections.Length == 2) return CurrentValue * 100; + + //TOHE的职业生成模式 + if (Selections.Length == 3) return CurrentValue; + + //0%~100%or5%~100%の場合 + var offset = 12 - Selections.Length; + var index = CurrentValue + offset; + var rate = index <= 1 ? index * 5 : (index - 1) * 10; + return rate; + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + // Setter + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + } + public override void SetValueNoRpc(int value) + { + base.SetValueNoRpc(Rule.RepeatIndex(value)); + } +} \ No newline at end of file diff --git a/Modules/OptionItem/TextOptionItem.cs b/Modules/OptionItem/TextOptionItem.cs new file mode 100644 index 000000000..b69af02ad --- /dev/null +++ b/Modules/OptionItem/TextOptionItem.cs @@ -0,0 +1,39 @@ +namespace TOHE; + +public class TextOptionItem : OptionItem +{ + // 必須情報 + public IntegerValueRule Rule; + + // コンストラクタ + public TextOptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue) + : base(id, name, defaultValue, tab, isSingleValue) + { + IsText = true; + IsHeader = true; + } + public static TextOptionItem Create( + int id, string name, TabGroup tab, bool isSingleValue = false + ) + { + return new TextOptionItem( + id, name, 0, tab, isSingleValue + ); + } + + // Getter + public override int GetInt() => Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return Translator.GetString(Name); + } + + // Setter + public override void SetValue(int value) + { + } + public override void SetValueNoRpc(int value) + { + } +} \ No newline at end of file diff --git a/Modules/OptionItem/ValueRule.cs b/Modules/OptionItem/ValueRule.cs new file mode 100644 index 000000000..66d223d35 --- /dev/null +++ b/Modules/OptionItem/ValueRule.cs @@ -0,0 +1,78 @@ +using System; + +namespace TOHE; + +public abstract class ValueRule +{ + public T MinValue { get; protected set; } + public T MaxValue { get; protected set; } + public T Step { get; protected set; } + + public ValueRule(T minValue, T maxValue, T step) + { + MinValue = minValue; + MaxValue = maxValue; + Step = step; + } + public ValueRule((T, T, T) tuple) + : this(tuple.Item1, tuple.Item2, tuple.Item3) + { } + + public abstract int RepeatIndex(int value); + public abstract T GetValueByIndex(int index); + public abstract int GetNearestIndex(T num); +} + +public class IntegerValueRule : ValueRule +{ + public IntegerValueRule(int minValue, int maxValue, int step) + : base(minValue, maxValue, step) { } + public IntegerValueRule((int, int, int) tuple) + : base(tuple) { } + + public static implicit operator IntegerValueRule((int, int, int) tuple) + => new(tuple); + + public override int RepeatIndex(int value) + { + int MaxIndex = (MaxValue - MinValue) / Step; + value = value % (MaxIndex + 1); + if (value < 0) value = MaxIndex; + return value; + } + + public override int GetValueByIndex(int index) + => RepeatIndex(index) * Step + MinValue; + + public override int GetNearestIndex(int num) + { + return (int)Math.Round((num - MinValue) / (float)Step); + } +} + +public class FloatValueRule : ValueRule +{ + public FloatValueRule(float minValue, float maxValue, float step) + : base(minValue, maxValue, step) { } + public FloatValueRule((float, float, float) tuple) + : base(tuple) { } + + public static implicit operator FloatValueRule((float, float, float) tuple) + => new(tuple); + + public override int RepeatIndex(int value) + { + int MaxIndex = (int)((MaxValue - MinValue) / Step); + value = value % (MaxIndex + 1); + if (value < 0) value = MaxIndex; + return value; + } + + public override float GetValueByIndex(int index) + => RepeatIndex(index) * Step + MinValue; + + public override int GetNearestIndex(float num) + { + return (int)Math.Round((num - MinValue) / Step); + } +} \ No newline at end of file diff --git a/Modules/OptionShower.cs b/Modules/OptionShower.cs new file mode 100644 index 000000000..82cf119c4 --- /dev/null +++ b/Modules/OptionShower.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +public static class OptionShower +{ + public static int currentPage = 0; + public static List pages = new(); + static OptionShower() + { + + } + public static string GetTextNoFresh() + { + if (pages.Count < 3) GetText(); + return $"{pages[currentPage]}{GetString("PressTabToNextPage")}({currentPage + 1}/{pages.Count})"; + } + public static string GetText() + { + //初期化 + StringBuilder sb = new(); + pages = new() + { + //1ページに基本ゲーム設定を格納 + GameOptionsManager.Instance.CurrentGameOptions.ToHudString(GameData.Instance ? GameData.Instance.PlayerCount : 10) + "\n\n" + }; + //ゲームモードの表示 + sb.Append($"{Options.GameMode.GetName()}: {Options.GameMode.GetString()}\n\n"); + if (Options.HideGameSettings.GetBool() && !AmongUsClient.Instance.AmHost) + { + sb.Append($"{GetString("Message.HideGameSettings")}"); + } + else + { + //Standardの時のみ実行 + if (Options.CurrentGameMode == CustomGameMode.Standard) + { + //有効な役職一覧 + sb.Append($"{Utils.GetRoleName(CustomRoles.GM)}: {Options.EnableGM.GetString()}\n\n"); + sb.Append(GetString("ActiveRolesList")).Append("\n"); + foreach (var kvp in Options.CustomRoleSpawnChances) + if (kvp.Value.GameMode is CustomGameMode.Standard or CustomGameMode.All && kvp.Value.GetBool()) //スタンダードか全てのゲームモードで表示する役職 + sb.Append($"{Utils.ColorString(Utils.GetRoleColor(kvp.Key), Utils.GetRoleName(kvp.Key))}: {kvp.Value.GetString()}×{kvp.Key.GetCount()}\n"); + pages.Add(sb.ToString() + "\n\n"); + sb.Clear(); + } + //有効な役職と詳細設定一覧 + pages.Add(""); + nameAndValue(Options.EnableGM); + foreach (var kvp in Options.CustomRoleSpawnChances) + { + if (!kvp.Key.IsEnable() || kvp.Value.IsHiddenOn(Options.CurrentGameMode)) continue; + sb.Append("\n"); + sb.Append($"{Utils.ColorString(Utils.GetRoleColor(kvp.Key), Utils.GetRoleName(kvp.Key))}: {kvp.Value.GetString()}×{kvp.Key.GetCount()}\n"); + ShowChildren(kvp.Value, ref sb, Utils.GetRoleColor(kvp.Key).ShadeColor(-0.5f), 1); + string rule = Utils.ColorString(Palette.ImpostorRed.ShadeColor(-0.5f), "┣ "); + string ruleFooter = Utils.ColorString(Palette.ImpostorRed.ShadeColor(-0.5f), "┗ "); + } + + foreach (var opt in OptionItem.AllOptions.Where(x => x.Id >= 90000 && !x.IsHiddenOn(Options.CurrentGameMode) && x.Parent == null && !x.IsText)) + { + if (opt.IsHeader) sb.Append("\n"); + sb.Append($"{opt.GetName()}: {opt.GetString()}\n"); + if (opt.GetBool()) + ShowChildren(opt, ref sb, Color.white, 1); + } + //Onの時に子要素まで表示するメソッド + void nameAndValue(OptionItem o) => sb.Append($"{o.GetName()}: {o.GetString()}\n"); + } + //1ページにつき35行までにする処理 + List tmp = new(sb.ToString().Split("\n\n")); + for (var i = 0; i < tmp.Count; i++) + { + if (pages[^1].Count(c => c == '\n') + 1 + tmp[i].Count(c => c == '\n') + 1 > 35) + pages.Add(tmp[i] + "\n\n"); + else pages[^1] += tmp[i] + "\n\n"; + } + if (currentPage >= pages.Count) currentPage = pages.Count - 1; //現在のページが最大ページ数を超えていれば最後のページに修正 + return $"{pages[currentPage]}{GetString("PressTabToNextPage")}({currentPage + 1}/{pages.Count})"; + } + public static void Next() + { + currentPage++; + if (currentPage >= pages.Count) currentPage = 0; //現在のページが最大ページを超えていれば最初のページに + } + private static void ShowChildren(OptionItem option, ref StringBuilder sb, Color color, int deep = 0) + { + foreach (var opt in option.Children.Select((v, i) => new { Value = v, Index = i + 1 })) + { + if (opt.Value.Name == "Maximum") continue; //Maximumの項目は飛ばす + sb.Append(string.Concat(Enumerable.Repeat(Utils.ColorString(color, "┃"), deep - 1))); + sb.Append(Utils.ColorString(color, opt.Index == option.Children.Count ? "┗ " : "┣ ")); + sb.Append($"{opt.Value.GetName()}: {opt.Value.GetString()}\n"); + if (opt.Value.GetBool()) ShowChildren(opt.Value, ref sb, color, deep + 1); + } + } +} \ No newline at end of file diff --git a/Modules/RPC.cs b/Modules/RPC.cs new file mode 100644 index 000000000..ccfe926ac --- /dev/null +++ b/Modules/RPC.cs @@ -0,0 +1,1137 @@ +using AmongUs.GameOptions; +using HarmonyLib; +using Hazel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TOHE.Modules; +using TOHE.Roles.AddOns.Crewmate; +using TOHE.Roles.AddOns.Impostor; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Impostor; +using TOHE.Roles.Neutral; +using static TOHE.Translator; +namespace TOHE; + +enum CustomRPC +{ + VersionCheck = 60, + RequestRetryVersionCheck = 61, + SyncCustomSettings = 80, + SetDeathReason, + EndGame, + PlaySound, + SetCustomRole, + SetBountyTarget, + SetKillOrSpell, + SetKillOrHex, + SetSheriffShotLimit, + SetCopyCatMiscopyLimit, + SetDousedPlayer, + setPlaguedPlayer, + SetNameColorData, + DoSpell, + DoHex, + SniperSync, + SetLoversPlayers, + SetExecutionerTarget, + RemoveExecutionerTarget, + SetLawyerTarget, + RemoveLawyerTarget, + SendFireWorksState, + SetCurrentDousingTarget, + SetEvilTrackerTarget, + SetRealKiller, + + // TOHE + AntiBlackout, + RestTOHESetting, + PlayCustomSound, + SetKillTimer, + SyncAllPlayerNames, + SyncNameNotify, + ShowPopUp, + KillFlash, + + //Roles + SetDrawPlayer, + SetCurrentDrawTarget, + SetGamerHealth, + SetPelicanEtenNum, + SwordsManKill, + SetCounterfeiterSellLimit, + SetPursuerSellLimit, + SetMedicalerProtectLimit, + SetGangsterRecruitLimit, + SetGhostPlayer, + SetDarkHiderKillCount, + SetEvilDiviner, + SetGreedierOE, + SetCursedWolfSpellCount, + SetJinxSpellCount, + SetCollectorVotes, + SetQuickShooterShotLimit, + SetEraseLimit, + GuessKill, + SetMarkedPlayer, + SetConcealerTimer, + SetMedicalerProtectList, + SetHackerHackLimit, + SyncPsychicRedList, + SetMorticianArrow, + SetTracefinderArrow, + Judge, + Guess, + MeetingKill, + MafiaRevenge, + RetributionistRevenge, + SetSwooperTimer, + SetWraithTimer, + SetBKTimer, + SyncTotocalcioTargetAndTimes, + SetSuccubusCharmLimit, + SetInfectiousBiteLimit, + SetCursedSoulCurseLimit, + SetMonarchKnightLimit, + SetDeputyHandcuffLimit, + SetVirusInfectLimit, + SetRevealedPlayer, + SetCurrentRevealTarget, + SetJackalRecruitLimit, + SetBloodhoundArrow, + SetVultureArrow, + SetSpiritcallerSpiritLimit, + SetDoomsayerProgress, + SetTrackerTarget, + + //SoloKombat + SyncKBPlayer, + SyncKBBackCountdown, + SyncKBNameNotify, + SetRitualist, + SetChameleonTimer, + DoPoison, + SetAdmireLimit, + SetRememberLimit, +} +public enum Sounds +{ + KillSound, + TaskComplete, + TaskUpdateSound, + ImpTransform, + + Test, +} +[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] +internal class RPCHandlerPatch +{ + public static bool TrustedRpc(byte id) + => (CustomRPC)id is CustomRPC.VersionCheck or CustomRPC.RequestRetryVersionCheck or CustomRPC.AntiBlackout or CustomRPC.Judge or CustomRPC.MeetingKill or CustomRPC.Guess or CustomRPC.MafiaRevenge or CustomRPC.RetributionistRevenge; + public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + var rpcType = (RpcCalls)callId; + MessageReader subReader = MessageReader.Get(reader); + if (EAC.ReceiveRpc(__instance, callId, reader)) return false; + Logger.Info($"{__instance?.Data?.PlayerId}({(__instance?.Data?.PlayerId == 0 ? "Host" : __instance?.Data?.PlayerName)}):{callId}({RPC.GetRpcName(callId)})", "ReceiveRPC"); + switch (rpcType) + { + case RpcCalls.SetName: //SetNameRPC + string name = subReader.ReadString(); + if (subReader.BytesRemaining > 0 && subReader.ReadBoolean()) return false; + Logger.Info("RPC名称修改:" + __instance.GetNameWithRole() + " => " + name, "SetName"); + break; + case RpcCalls.SetRole: //SetNameRPC + var role = (RoleTypes)subReader.ReadUInt16(); + Logger.Info("RPC设置职业:" + __instance.GetRealName() + " => " + role, "SetRole"); + break; + case RpcCalls.SendChat: + var text = subReader.ReadString(); + Logger.Info($"{__instance.GetNameWithRole()}:{text}", "ReceiveChat"); + ChatCommands.OnReceiveChat(__instance, text, out var canceled); + if (canceled) return false; + break; + case RpcCalls.StartMeeting: + var p = Utils.GetPlayerById(subReader.ReadByte()); + Logger.Info($"{__instance.GetNameWithRole()} => {p?.GetNameWithRole() ?? "null"}", "StartMeeting"); + break; + } + if (__instance.PlayerId != 0 + && Enum.IsDefined(typeof(CustomRPC), (int)callId) + && !TrustedRpc(callId)) //ホストではなく、CustomRPCで、VersionCheckではない + { + Logger.Warn($"{__instance?.Data?.PlayerName}:{callId}({RPC.GetRpcName(callId)}) 已取消,因为它是由主机以外的其他人发送的。", "CustomRPC"); + if (AmongUsClient.Instance.AmHost) + { + if (!EAC.ReceiveInvalidRpc(__instance, callId)) return false; + AmongUsClient.Instance.KickPlayer(__instance.GetClientId(), false); + Logger.Warn($"收到来自 {__instance?.Data?.PlayerName} 的不受信用的RPC,因此将其踢出。", "Kick"); + Logger.SendInGame(string.Format(GetString("Warning.InvalidRpc"), __instance?.Data?.PlayerName)); + } + return false; + } + return true; + } + public static void Postfix(PlayerControl __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + var rpcType = (CustomRPC)callId; + switch (rpcType) + { + case CustomRPC.AntiBlackout: + if (Options.EndWhenPlayerBug.GetBool()) + { + Logger.Fatal($"{__instance?.Data?.PlayerName}({__instance.PlayerId}): {reader.ReadString()} 错误,根据设定终止游戏", "Anti-black"); + ChatUpdatePatch.DoBlockChat = true; + Main.OverrideWelcomeMsg = string.Format(GetString("RpcAntiBlackOutNotifyInLobby"), __instance?.Data?.PlayerName, GetString("EndWhenPlayerBug")); + new LateTask(() => + { + Logger.SendInGame(string.Format(GetString("RpcAntiBlackOutEndGame"), __instance?.Data?.PlayerName), true); + }, 3f, "Anti-Black Msg SendInGame"); + new LateTask(() => + { + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Error); + GameManager.Instance.LogicFlow.CheckEndCriteria(); + RPC.ForceEndGame(CustomWinner.Error); + }, 5.5f, "Anti-Black End Game"); + } + else if (GameStates.IsOnlineGame) + { + Logger.Fatal($"{__instance?.Data?.PlayerName}({__instance.PlayerId}): Change Role Setting Postfix 错误,根据设定继续游戏", "Anti-black"); + new LateTask(() => + { + Logger.SendInGame(string.Format(GetString("RpcAntiBlackOutIgnored"), __instance?.Data?.PlayerName), true); + }, 3f, "Anti-Black Msg SendInGame"); + } + break; + case CustomRPC.VersionCheck: + try + { + Version version = Version.Parse(reader.ReadString()); + string tag = reader.ReadString(); + string forkId = reader.ReadString(); + Main.playerVersion[__instance.PlayerId] = new PlayerVersion(version, tag, forkId); + + if (Main.VersionCheat.Value && __instance.PlayerId == 0) RPC.RpcVersionCheck(); + + if (Main.VersionCheat.Value && AmongUsClient.Instance.AmHost) + Main.playerVersion[__instance.PlayerId] = Main.playerVersion[0]; + + // Kick Unmached Player Start + if (AmongUsClient.Instance.AmHost && tag != $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})") + { + if (forkId != Main.ForkId) + new LateTask(() => + { + if (__instance?.Data?.Disconnected is not null and not true) + { + var msg = string.Format(GetString("KickBecauseDiffrentVersionOrMod"), __instance?.Data?.PlayerName); + Logger.Warn(msg, "Version Kick"); + Logger.SendInGame(msg); + AmongUsClient.Instance.KickPlayer(__instance.GetClientId(), false); + } + }, 5f, "Kick"); + } + // Kick Unmached Player End + } + catch + { + Logger.Warn($"{__instance?.Data?.PlayerName}({__instance.PlayerId}): バージョン情報が無効です", "RpcVersionCheck"); + new LateTask(() => + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.RequestRetryVersionCheck, SendOption.Reliable, __instance.GetClientId()); + AmongUsClient.Instance.FinishRpcImmediately(writer); + }, 1f, "Retry Version Check Task"); + } + break; + case CustomRPC.RequestRetryVersionCheck: + RPC.RpcVersionCheck(); + break; + case CustomRPC.SyncCustomSettings: + if (AmongUsClient.Instance.AmHost) break; + List list = new(); + var startAmount = reader.ReadInt32(); + var lastAmount = reader.ReadInt32(); + for (var i = startAmount; i < OptionItem.AllOptions.Count && i <= lastAmount; i++) + list.Add(OptionItem.AllOptions[i]); + Logger.Info($"{startAmount}-{lastAmount}:{list.Count}/{OptionItem.AllOptions.Count}", "SyncCustomSettings"); + foreach (var co in list) co.SetValue(reader.ReadInt32()); + OptionShower.GetText(); + break; + case CustomRPC.SetDeathReason: + RPC.GetDeathReason(reader); + break; + case CustomRPC.EndGame: + RPC.EndGame(reader); + break; + case CustomRPC.PlaySound: + byte playerID = reader.ReadByte(); + Sounds sound = (Sounds)reader.ReadByte(); + RPC.PlaySound(playerID, sound); + break; + case CustomRPC.ShowPopUp: + string msg = reader.ReadString(); + HudManager.Instance.ShowPopUp(msg); + break; + case CustomRPC.SetCustomRole: + byte CustomRoleTargetId = reader.ReadByte(); + CustomRoles role = (CustomRoles)reader.ReadPackedInt32(); + RPC.SetCustomRole(CustomRoleTargetId, role); + break; + case CustomRPC.SetBountyTarget: + BountyHunter.ReceiveRPC(reader); + break; + case CustomRPC.SetKillOrSpell: + Witch.ReceiveRPC(reader, false); + break; + case CustomRPC.SetKillOrHex: + HexMaster.ReceiveRPC(reader, false); + break; + + case CustomRPC.SetSheriffShotLimit: + Sheriff.ReceiveRPC(reader); + break; + case CustomRPC.SetCopyCatMiscopyLimit: + CopyCat.ReceiveRPC(reader); + break; + case CustomRPC.SetDousedPlayer: + byte ArsonistId = reader.ReadByte(); + byte DousedId = reader.ReadByte(); + bool doused = reader.ReadBoolean(); + Main.isDoused[(ArsonistId, DousedId)] = doused; + break; + case CustomRPC.setPlaguedPlayer: + PlagueBearer.receiveRPC(reader); + break; + case CustomRPC.SetDrawPlayer: + byte RevolutionistId = reader.ReadByte(); + byte DrawId = reader.ReadByte(); + bool drawed = reader.ReadBoolean(); + Main.isDraw[(RevolutionistId, DrawId)] = drawed; + break; + case CustomRPC.SetRevealedPlayer: + byte FarseerId = reader.ReadByte(); + byte RevealId = reader.ReadByte(); + bool revealed = reader.ReadBoolean(); + Main.isRevealed[(FarseerId, RevealId)] = revealed; + break; + case CustomRPC.SetNameColorData: + NameColorManager.ReceiveRPC(reader); + break; + case CustomRPC.DoSpell: + Witch.ReceiveRPC(reader, true); + break; + case CustomRPC.DoHex: + HexMaster.ReceiveRPC(reader, true); + break; + case CustomRPC.SniperSync: + Sniper.ReceiveRPC(reader); + break; + case CustomRPC.SetLoversPlayers: + Main.LoversPlayers.Clear(); + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + Main.LoversPlayers.Add(Utils.GetPlayerById(reader.ReadByte())); + break; + case CustomRPC.SetExecutionerTarget: + Executioner.ReceiveRPC(reader, SetTarget: true); + break; + case CustomRPC.RemoveExecutionerTarget: + Executioner.ReceiveRPC(reader, SetTarget: false); + break; + case CustomRPC.SetLawyerTarget: + Lawyer.ReceiveRPC(reader, SetTarget: true); + break; + case CustomRPC.RemoveLawyerTarget: + Lawyer.ReceiveRPC(reader, SetTarget: false); + break; + case CustomRPC.SendFireWorksState: + FireWorks.ReceiveRPC(reader); + break; + case CustomRPC.SetCurrentDousingTarget: + byte arsonistId = reader.ReadByte(); + byte dousingTargetId = reader.ReadByte(); + if (PlayerControl.LocalPlayer.PlayerId == arsonistId) + Main.currentDousingTarget = dousingTargetId; + break; + case CustomRPC.SetCurrentDrawTarget: + byte arsonistId1 = reader.ReadByte(); + byte doTargetId = reader.ReadByte(); + if (PlayerControl.LocalPlayer.PlayerId == arsonistId1) + Main.currentDrawTarget = doTargetId; + break; + case CustomRPC.SetEvilTrackerTarget: + EvilTracker.ReceiveRPC(reader); + break; + case CustomRPC.SetRealKiller: + byte targetId = reader.ReadByte(); + byte killerId = reader.ReadByte(); + RPC.SetRealKiller(targetId, killerId); + break; + case CustomRPC.SetGamerHealth: + Gamer.ReceiveRPC(reader); + break; + case CustomRPC.SetPelicanEtenNum: + Pelican.ReceiveRPC(reader); + break; + case CustomRPC.SetDoomsayerProgress: + Doomsayer.ReceiveRPC(reader); + break; + case CustomRPC.SwordsManKill: + SwordsMan.ReceiveRPC(reader); + break; + case CustomRPC.SetCounterfeiterSellLimit: + Counterfeiter.ReceiveRPC(reader); + break; + case CustomRPC.SetPursuerSellLimit: + Pursuer.ReceiveRPC(reader); + break; + case CustomRPC.SetMedicalerProtectLimit: + Medic.ReceiveRPC(reader); + break; + case CustomRPC.SetGangsterRecruitLimit: + Gangster.ReceiveRPC(reader); + break; + case CustomRPC.SetJackalRecruitLimit: + Jackal.ReceiveRPC(reader); + break; + case CustomRPC.SetAdmireLimit: + Admirer.ReceiveRPC(reader); + break; + case CustomRPC.SetRememberLimit: + Amnesiac.ReceiveRPC(reader); + break; + case CustomRPC.PlayCustomSound: + CustomSoundsManager.ReceiveRPC(reader); + break; + case CustomRPC.SetGhostPlayer: + BallLightning.ReceiveRPC(reader); + break; + case CustomRPC.SetDarkHiderKillCount: + DarkHide.ReceiveRPC(reader); + break; + case CustomRPC.SetGreedierOE: + Greedier.ReceiveRPC(reader); + break; + case CustomRPC.SetCursedWolfSpellCount: + byte CursedWolfId = reader.ReadByte(); + int GuardNum = reader.ReadInt32(); + if (Main.CursedWolfSpellCount.ContainsKey(CursedWolfId)) + Main.CursedWolfSpellCount[CursedWolfId] = GuardNum; + else + Main.CursedWolfSpellCount.Add(CursedWolfId, Options.GuardSpellTimes.GetInt()); + break; + case CustomRPC.SetJinxSpellCount: + byte JinxId = reader.ReadByte(); + int JinxGuardNum = reader.ReadInt32(); + if (Main.JinxSpellCount.ContainsKey(JinxId)) + Main.JinxSpellCount[JinxId] = JinxGuardNum; + else + Main.JinxSpellCount.Add(JinxId, Jinx.JinxSpellTimes.GetInt()); + break; + case CustomRPC.SetCollectorVotes: + Collector.ReceiveRPC(reader); + break; + case CustomRPC.SetQuickShooterShotLimit: + QuickShooter.ReceiveRPC(reader); + break; + case CustomRPC.RestTOHESetting: + OptionItem.AllOptions.ToArray().Where(x => x.Id > 0).Do(x => x.SetValueNoRpc(x.DefaultValue)); + OptionShower.GetText(); + break; + case CustomRPC.SetEraseLimit: + Eraser.ReceiveRPC(reader); + break; + case CustomRPC.GuessKill: + GuessManager.RpcClientGuess(Utils.GetPlayerById(reader.ReadByte())); + break; + case CustomRPC.SetMarkedPlayer: + Assassin.ReceiveRPC(reader); + break; + case CustomRPC.SetMedicalerProtectList: + Medic.ReceiveRPCForProtectList(reader); + break; + case CustomRPC.SetHackerHackLimit: + Hacker.ReceiveRPC(reader); + break; + case CustomRPC.SyncPsychicRedList: + Psychic.ReceiveRPC(reader); + break; + case CustomRPC.SetKillTimer: + float time = reader.ReadSingle(); + PlayerControl.LocalPlayer.SetKillTimer(time); + break; + case CustomRPC.SyncKBPlayer: + SoloKombatManager.ReceiveRPCSyncKBPlayer(reader); + break; + case CustomRPC.SyncAllPlayerNames: + Main.AllPlayerNames = new(); + int num = reader.ReadInt32(); + for (int i = 0; i < num; i++) + Main.AllPlayerNames.TryAdd(reader.ReadByte(), reader.ReadString()); + break; + case CustomRPC.SyncKBBackCountdown: + SoloKombatManager.ReceiveRPCSyncBackCountdown(reader); + break; + case CustomRPC.SyncKBNameNotify: + SoloKombatManager.ReceiveRPCSyncNameNotify(reader); + break; + case CustomRPC.SetMorticianArrow: + Mortician.ReceiveRPC(reader); + break; + case CustomRPC.SetTracefinderArrow: + Tracefinder.ReceiveRPC(reader); + break; + case CustomRPC.SyncNameNotify: + NameNotifyManager.ReceiveRPC(reader); + break; + case CustomRPC.Judge: + Judge.ReceiveRPC(reader, __instance); + break; + case CustomRPC.MeetingKill: + Councillor.ReceiveRPC(reader, __instance); + break; + case CustomRPC.Guess: + GuessManager.ReceiveRPC(reader, __instance); + break; + case CustomRPC.MafiaRevenge: + MafiaRevengeManager.ReceiveRPC(reader, __instance); + break; + case CustomRPC.RetributionistRevenge: + RetributionistRevengeManager.ReceiveRPC(reader, __instance); + break; + case CustomRPC.SetSwooperTimer: + Swooper.ReceiveRPC(reader); + break; + case CustomRPC.SetWraithTimer: + Wraith.ReceiveRPC(reader); + break; + case CustomRPC.SetChameleonTimer: + Chameleon.ReceiveRPC(reader); + break; + case CustomRPC.SetBKTimer: + BloodKnight.ReceiveRPC(reader); + break; + case CustomRPC.SyncTotocalcioTargetAndTimes: + Totocalcio.ReceiveRPC(reader); + break; + case CustomRPC.SetSuccubusCharmLimit: + Succubus.ReceiveRPC(reader); + break; + case CustomRPC.SetCursedSoulCurseLimit: + CursedSoul.ReceiveRPC(reader); + break; + case CustomRPC.SetInfectiousBiteLimit: + Infectious.ReceiveRPC(reader); + break; + case CustomRPC.SetMonarchKnightLimit: + Monarch.ReceiveRPC(reader); + break; + case CustomRPC.SetEvilDiviner: + EvilDiviner.ReceiveRPC(reader); + break; + case CustomRPC.SetRitualist: + Ritualist.ReceiveRPC(reader); + break; + case CustomRPC.SetDeputyHandcuffLimit: + Deputy.ReceiveRPC(reader); + break; + case CustomRPC.SetVirusInfectLimit: + Virus.ReceiveRPC(reader); + break; + case CustomRPC.KillFlash: + Utils.FlashColor(new(1f, 0f, 0f, 0.3f)); + if (Constants.ShouldPlaySfx()) RPC.PlaySound(PlayerControl.LocalPlayer.PlayerId, Sounds.KillSound); + break; + case CustomRPC.SetBloodhoundArrow: + Bloodhound.ReceiveRPC(reader); + break; + case CustomRPC.SetVultureArrow: + Vulture.ReceiveRPC(reader); + break; + case CustomRPC.DoPoison: + Baker.ReceiveRPC(reader); + break; + case CustomRPC.SetSpiritcallerSpiritLimit: + Spiritcaller.ReceiveRPC(reader); + break; + case CustomRPC.SetTrackerTarget: + Tracker.ReceiveRPC(reader); + break; + } + } +} + +internal static class RPC +{ + //来源:https://github.com/music-discussion/TownOfHost-TheOtherRoles/blob/main/Modules/RPC.cs + public static void SyncCustomSettingsRPC(int targetId = -1) + { + if (targetId != -1) + { + var client = Utils.GetClientById(targetId); + if (client == null || client.Character == null || !Main.playerVersion.ContainsKey(client.Character.PlayerId)) return; + } + if (!AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls.Count <= 1 || (AmongUsClient.Instance.AmHost == false && PlayerControl.LocalPlayer == null)) return; + var amount = OptionItem.AllOptions.Count; + int divideBy = amount / 10; + for (var i = 0; i <= 10; i++) + SyncOptionsBetween(i * divideBy, (i + 1) * divideBy, targetId); + } + public static void SyncCustomSettingsRPCforOneOption(OptionItem option) + { + List allOptions = new(OptionItem.AllOptions); + var placement = allOptions.IndexOf(option); + if (placement != -1) + SyncOptionsBetween(placement, placement); + } + static void SyncOptionsBetween(int startAmount, int lastAmount, int targetId = -1) + { + if (targetId != -1) + { + var client = Utils.GetClientById(targetId); + if (client == null || client.Character == null || !Main.playerVersion.ContainsKey(client.Character.PlayerId)) return; + } + if (!AmongUsClient.Instance.AmHost || PlayerControl.AllPlayerControls.Count <= 1 || (AmongUsClient.Instance.AmHost == false && PlayerControl.LocalPlayer == null)) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, 80, SendOption.Reliable, targetId); + List list = new(); + writer.Write(startAmount); + writer.Write(lastAmount); + for (var i = startAmount; i < OptionItem.AllOptions.Count && i <= lastAmount; i++) + list.Add(OptionItem.AllOptions[i]); + Logger.Info($"{startAmount}-{lastAmount}:{list.Count}/{OptionItem.AllOptions.Count}", "SyncCustomSettings"); + foreach (var co in list) writer.Write(co.GetValue()); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void PlaySoundRPC(byte PlayerID, Sounds sound) + { + if (AmongUsClient.Instance.AmHost) + PlaySound(PlayerID, sound); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.PlaySound, Hazel.SendOption.Reliable, -1); + writer.Write(PlayerID); + writer.Write((byte)sound); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void SyncAllPlayerNames() + { + if (!AmongUsClient.Instance.AmHost) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncAllPlayerNames, SendOption.Reliable, -1); + writer.Write(Main.AllPlayerNames.Count); + foreach (var name in Main.AllPlayerNames) + { + writer.Write(name.Key); + writer.Write(name.Value); + } + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ShowPopUp(this PlayerControl pc, string msg) + { + if (!AmongUsClient.Instance.AmHost) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.ShowPopUp, SendOption.Reliable, pc.GetClientId()); + writer.Write(msg); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ExileAsync(PlayerControl player) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(player.NetId, (byte)RpcCalls.Exiled, SendOption.Reliable, -1); + AmongUsClient.Instance.FinishRpcImmediately(writer); + player.Exiled(); + } + public static async void RpcVersionCheck() + { + while (PlayerControl.LocalPlayer == null) await Task.Delay(500); + if (Main.playerVersion.ContainsKey(0) || !Main.VersionCheat.Value) + { + bool cheating = Main.VersionCheat.Value; + MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.VersionCheck, SendOption.Reliable); + writer.Write(cheating ? Main.playerVersion[0].version.ToString() : Main.PluginVersion); + writer.Write(cheating ? Main.playerVersion[0].tag : $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})"); + writer.Write(cheating ? Main.playerVersion[0].forkId : Main.ForkId); + writer.EndMessage(); + } + Main.playerVersion[PlayerControl.LocalPlayer.PlayerId] = new PlayerVersion(Main.PluginVersion, $"{ThisAssembly.Git.Commit}({ThisAssembly.Git.Branch})", Main.ForkId); + } + public static void SendDeathReason(byte playerId, PlayerState.DeathReason deathReason) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetDeathReason, SendOption.Reliable, -1); + writer.Write(playerId); + writer.Write((int)deathReason); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void GetDeathReason(MessageReader reader) + { + var playerId = reader.ReadByte(); + var deathReason = (PlayerState.DeathReason)reader.ReadInt32(); + Main.PlayerStates[playerId].deathReason = deathReason; + Main.PlayerStates[playerId].IsDead = true; + } + public static void ForceEndGame(CustomWinner win) + { + if (ShipStatus.Instance == null) return; + try { CustomWinnerHolder.ResetAndSetWinner(win); } + catch { } + if (AmongUsClient.Instance.AmHost) + { + ShipStatus.Instance.enabled = false; + try { GameManager.Instance.LogicFlow.CheckEndCriteria(); } + catch { } + try { GameManager.Instance.RpcEndGame(GameOverReason.ImpostorDisconnect, false); } + catch { } + } + } + public static void EndGame(MessageReader reader) + { + try + { + CustomWinnerHolder.ReadFrom(reader); + } + catch (Exception ex) + { + Logger.Error($"正常にEndGameを行えませんでした。\n{ex}", "EndGame", false); + } + } + public static void PlaySound(byte playerID, Sounds sound) + { + if (PlayerControl.LocalPlayer.PlayerId == playerID) + { + switch (sound) + { + case Sounds.KillSound: + SoundManager.Instance.PlaySound(PlayerControl.LocalPlayer.KillSfx, false, 1f); + break; + case Sounds.TaskComplete: + SoundManager.Instance.PlaySound(DestroyableSingleton.Instance.TaskCompleteSound, false, 1f); + break; + case Sounds.TaskUpdateSound: + SoundManager.Instance.PlaySound(DestroyableSingleton.Instance.TaskUpdateSound, false, 1f); + break; + case Sounds.ImpTransform: + SoundManager.Instance.PlaySound(DestroyableSingleton.Instance.HnSOtherImpostorTransformSfx, false, 0.8f); + break; + } + } + } + public static void SetCustomRole(byte targetId, CustomRoles role) + { + if (role < CustomRoles.NotAssigned) + { + Main.PlayerStates[targetId].SetMainRole(role); + } + else if (role >= CustomRoles.NotAssigned) //500:NoSubRole 501~:SubRole + { + Main.PlayerStates[targetId].SetSubRole(role); + } + switch (role) + { + case CustomRoles.BountyHunter: + BountyHunter.Add(targetId); + break; + case CustomRoles.SerialKiller: + SerialKiller.Add(targetId); + break; + case CustomRoles.FireWorks: + FireWorks.Add(targetId); + break; + case CustomRoles.TimeThief: + TimeThief.Add(targetId); + break; + case CustomRoles.Sniper: + Sniper.Add(targetId); + break; + case CustomRoles.Crusader: + Crusader.Add(targetId); + break; + /* case CustomRoles.Mare: + Mare.Add(targetId); + break; */ + case CustomRoles.EvilTracker: + EvilTracker.Add(targetId); + break; + case CustomRoles.Witch: + Witch.Add(targetId); + break; + case CustomRoles.Vampire: + Vampire.Add(targetId); + break; + case CustomRoles.Executioner: + Executioner.Add(targetId); + break; + case CustomRoles.Farseer: + Farseer.Add(targetId); + break; + case CustomRoles.Lawyer: + Lawyer.Add(targetId); + break; + case CustomRoles.HexMaster: + HexMaster.Add(targetId); + break; + case CustomRoles.Jackal: + Jackal.Add(targetId); + break; + case CustomRoles.Sidekick: + Sidekick.Add(targetId); + break; + case CustomRoles.Poisoner: + Poisoner.Add(targetId); + break; + case CustomRoles.Sheriff: + Sheriff.Add(targetId); + break; + case CustomRoles.CopyCat: + CopyCat.Add(targetId); + break; + case CustomRoles.QuickShooter: + QuickShooter.Add(targetId); + break; + case CustomRoles.SwordsMan: + SwordsMan.Add(targetId); + break; + case CustomRoles.SabotageMaster: + SabotageMaster.Add(targetId); + break; + case CustomRoles.Snitch: + Snitch.Add(targetId); + break; + case CustomRoles.Marshall: + Marshall.Add(targetId); + break; + case CustomRoles.AntiAdminer: + AntiAdminer.Add(targetId); + break; + case CustomRoles.LastImpostor: + LastImpostor.Add(targetId); + break; + case CustomRoles.TimeManager: + TimeManager.Add(targetId); + break; + case CustomRoles.Workhorse: + Workhorse.Add(targetId); + break; + case CustomRoles.Pelican: + Pelican.Add(targetId); + break; + case CustomRoles.Counterfeiter: + Counterfeiter.Add(targetId); + break; + case CustomRoles.Pursuer: + Pursuer.Add(targetId); + break; + case CustomRoles.Gangster: + Gangster.Add(targetId); + break; + case CustomRoles.EvilDiviner: + EvilDiviner.Add(targetId); + break; + case CustomRoles.Medic: + Medic.Add(targetId); + break; + case CustomRoles.Divinator: + Divinator.Add(targetId); + break; + case CustomRoles.Oracle: + Oracle.Add(targetId); + break; + case CustomRoles.Gamer: + Gamer.Add(targetId); + break; + case CustomRoles.BallLightning: + BallLightning.Add(targetId); + break; + case CustomRoles.DarkHide: + DarkHide.Add(targetId); + break; + case CustomRoles.Greedier: + Greedier.Add(targetId); + break; + case CustomRoles.Collector: + Collector.Add(targetId); + break; + case CustomRoles.CursedWolf: + Main.CursedWolfSpellCount[targetId] = Options.GuardSpellTimes.GetInt(); + break; + case CustomRoles.Jinx: + Main.JinxSpellCount[targetId] = Jinx.JinxSpellTimes.GetInt(); + Jinx.Add(targetId); + break; + case CustomRoles.Eraser: + Eraser.Add(targetId); + break; + case CustomRoles.Assassin: + Assassin.Add(targetId); + break; + case CustomRoles.Sans: + Sans.Add(targetId); + break; + case CustomRoles.Juggernaut: + Juggernaut.Add(targetId); + break; + case CustomRoles.Reverie: + Reverie.Add(targetId); + break; + case CustomRoles.Hacker: + Hacker.Add(targetId); + break; + case CustomRoles.Psychic: + Psychic.Add(targetId); + break; + case CustomRoles.Hangman: + Hangman.Add(targetId); + break; + case CustomRoles.Judge: + Judge.Add(targetId); + break; + case CustomRoles.ParityCop: + ParityCop.Add(targetId); + break; + case CustomRoles.Baker: + Baker.Add(targetId); + break; + case CustomRoles.Councillor: + Councillor.Add(targetId); + break; + case CustomRoles.Mortician: + Mortician.Add(targetId); + break; + case CustomRoles.Tracefinder: + Tracefinder.Add(targetId); + break; + case CustomRoles.Mediumshiper: + Mediumshiper.Add(targetId); + break; + case CustomRoles.Veteran: + Main.VeteranNumOfUsed.Add(targetId, Options.VeteranSkillMaxOfUseage.GetInt()); + break; + case CustomRoles.Swooper: + Swooper.Add(targetId); + break; + case CustomRoles.Wraith: + Wraith.Add(targetId); + break; + case CustomRoles.Chameleon: + Chameleon.Add(targetId); + break; + case CustomRoles.BloodKnight: + BloodKnight.Add(targetId); + break; + case CustomRoles.Totocalcio: + Totocalcio.Add(targetId); + break; + case CustomRoles.Succubus: + Succubus.Add(targetId); + break; + case CustomRoles.CursedSoul: + CursedSoul.Add(targetId); + break; + case CustomRoles.Admirer: + Admirer.Add(targetId); + break; + case CustomRoles.Amnesiac: + Amnesiac.Add(targetId); + break; + case CustomRoles.DovesOfNeace: + Main.DovesOfNeaceNumOfUsed.Add(targetId, Options.DovesOfNeaceMaxOfUseage.GetInt()); + break; + case CustomRoles.Infectious: + Infectious.Add(targetId); + break; + case CustomRoles.Monarch: + Monarch.Add(targetId); + break; + case CustomRoles.Deputy: + Deputy.Add(targetId); + break; + case CustomRoles.Virus: + Virus.Add(targetId); + break; + case CustomRoles.Bloodhound: + Bloodhound.Add(targetId); + break; + case CustomRoles.Vulture: + Vulture.Add(targetId); + break; + case CustomRoles.PlagueBearer: + PlagueBearer.Add(targetId); + break; + case CustomRoles.Tracker: + Tracker.Add(targetId); + break; + case CustomRoles.Merchant: + Merchant.Add(targetId); + break; + case CustomRoles.NSerialKiller: + NSerialKiller.Add(targetId); + break; + case CustomRoles.Traitor: + Traitor.Add(targetId); + break; + case CustomRoles.NWitch: + NWitch.Add(targetId); + break; + case CustomRoles.Maverick: + Maverick.Add(targetId); + break; + case CustomRoles.Dazzler: + Dazzler.Add(targetId); + break; + case CustomRoles.Addict: + Addict.Add(targetId); + break; + case CustomRoles.Deathpact: + Deathpact.Add(targetId); + break; + case CustomRoles.Wildling: + Wildling.Add(targetId); + break; + case CustomRoles.Morphling: + Morphling.Add(targetId); + break; + case CustomRoles.Devourer: + Devourer.Add(targetId); + break; + case CustomRoles.Spiritualist: + Spiritualist.Add(targetId); + break; + case CustomRoles.Spiritcaller: + Spiritcaller.Add(targetId); + break; + case CustomRoles.Lurker: + Lurker.Add(targetId); + break; + case CustomRoles.Doomsayer: + Doomsayer.Add(targetId); + break; + case CustomRoles.Pirate: + Pirate.Add(targetId); + break; + } + HudManager.Instance.SetHudActive(true); + if (PlayerControl.LocalPlayer.PlayerId == targetId) RemoveDisableDevicesPatch.UpdateDisableDevices(); + } + public static void RpcDoSpell(byte targetId, byte killerId) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.DoSpell, Hazel.SendOption.Reliable, -1); + writer.Write(targetId); + writer.Write(killerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void SyncLoversPlayers() + { + if (!AmongUsClient.Instance.AmHost) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetLoversPlayers, Hazel.SendOption.Reliable, -1); + writer.Write(Main.LoversPlayers.Count); + foreach (var lp in Main.LoversPlayers) + { + writer.Write(lp.PlayerId); + } + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void SendRpcLogger(uint targetNetId, byte callId, int targetClientId = -1) + { + if (!DebugModeManager.AmDebugger) return; + string rpcName = GetRpcName(callId); + string from = targetNetId.ToString(); + string target = targetClientId.ToString(); + try + { + target = targetClientId < 0 ? "All" : AmongUsClient.Instance.GetClient(targetClientId).PlayerName; + from = Main.AllPlayerControls.Where(c => c.NetId == targetNetId).FirstOrDefault()?.Data?.PlayerName; + } + catch { } + Logger.Info($"FromNetID:{targetNetId}({from}) TargetClientID:{targetClientId}({target}) CallID:{callId}({rpcName})", "SendRPC"); + } + public static string GetRpcName(byte callId) + { + string rpcName; + if ((rpcName = Enum.GetName(typeof(RpcCalls), callId)) != null) { } + else if ((rpcName = Enum.GetName(typeof(CustomRPC), callId)) != null) { } + else rpcName = callId.ToString(); + return rpcName; + } + public static void SetCurrentDousingTarget(byte arsonistId, byte targetId) + { + if (PlayerControl.LocalPlayer.PlayerId == arsonistId) + { + Main.currentDousingTarget = targetId; + } + else + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCurrentDousingTarget, Hazel.SendOption.Reliable, -1); + writer.Write(arsonistId); + writer.Write(targetId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } + public static void SetCurrentDrawTarget(byte arsonistId, byte targetId) + { + if (PlayerControl.LocalPlayer.PlayerId == arsonistId) + { + Main.currentDrawTarget = targetId; + } + else + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCurrentDrawTarget, Hazel.SendOption.Reliable, -1); + writer.Write(arsonistId); + writer.Write(targetId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } + public static void SetCurrentRevealTarget(byte arsonistId, byte targetId) + { + if (PlayerControl.LocalPlayer.PlayerId == arsonistId) + { + Main.currentDrawTarget = targetId; + } + else + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCurrentRevealTarget, Hazel.SendOption.Reliable, -1); + writer.Write(arsonistId); + writer.Write(targetId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } + public static void SendRPCCursedWolfSpellCount(byte playerId) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetCursedWolfSpellCount, SendOption.Reliable, -1); + writer.Write(playerId); + writer.Write(Main.CursedWolfSpellCount[playerId]); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void SendRPCJinxSpellCount(byte playerId) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetJinxSpellCount, SendOption.Reliable, -1); + writer.Write(playerId); + writer.Write(Main.JinxSpellCount[playerId]); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ResetCurrentDousingTarget(byte arsonistId) => SetCurrentDousingTarget(arsonistId, 255); + public static void ResetCurrentDrawTarget(byte arsonistId) => SetCurrentDrawTarget(arsonistId, 255); + public static void ResetCurrentRevealTarget(byte arsonistId) => SetCurrentRevealTarget(arsonistId, 255); + public static void SetRealKiller(byte targetId, byte killerId) + { + var state = Main.PlayerStates[targetId]; + state.RealKiller.Item1 = DateTime.Now; + state.RealKiller.Item2 = killerId; + + if (!AmongUsClient.Instance.AmHost) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SetRealKiller, Hazel.SendOption.Reliable, -1); + writer.Write(targetId); + writer.Write(killerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } +} +[HarmonyPatch(typeof(InnerNet.InnerNetClient), nameof(InnerNet.InnerNetClient.StartRpc))] +internal class StartRpcPatch +{ + public static void Prefix(InnerNet.InnerNetClient __instance, [HarmonyArgument(0)] uint targetNetId, [HarmonyArgument(1)] byte callId) + { + RPC.SendRpcLogger(targetNetId, callId); + } +} +[HarmonyPatch(typeof(InnerNet.InnerNetClient), nameof(InnerNet.InnerNetClient.StartRpcImmediately))] +internal class StartRpcImmediatelyPatch +{ + public static void Prefix(InnerNet.InnerNetClient __instance, [HarmonyArgument(0)] uint targetNetId, [HarmonyArgument(1)] byte callId, [HarmonyArgument(3)] int targetClientId = -1) + { + RPC.SendRpcLogger(targetNetId, callId, targetClientId); + } +} \ No newline at end of file diff --git a/Modules/Random/HashRandomWrapper.cs b/Modules/Random/HashRandomWrapper.cs new file mode 100644 index 000000000..2e03f6a8e --- /dev/null +++ b/Modules/Random/HashRandomWrapper.cs @@ -0,0 +1,11 @@ +namespace TOHE; + +public class HashRandomWrapper : IRandom +{ + public HashRandomWrapper() { } + + public int Next(int minValue, int maxValue) => HashRandom.Next(minValue, maxValue); + public int Next(int maxValue) => HashRandom.Next(maxValue); + public uint Next() => HashRandom.Next(); + public int FastNext(int maxValue) => HashRandom.FastNext(maxValue); +} \ No newline at end of file diff --git a/Modules/Random/IRandom.cs b/Modules/Random/IRandom.cs new file mode 100644 index 000000000..e20b4a661 --- /dev/null +++ b/Modules/Random/IRandom.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace TOHE; + +public interface IRandom +{ + /// 0以上maxValue未満の乱数を生成します。 + public int Next(int maxValue); + /// minValue以上maxValue未満の乱数を生成します。 + public int Next(int minValue, int maxValue); + + // == static == + // IRandomを実装するクラスのリスト + public static Dictionary randomTypes = new() + { + { 0, typeof(NetRandomWrapper) }, //Default + { 1, typeof(NetRandomWrapper) }, + { 2, typeof(HashRandomWrapper) }, + { 3, typeof(Xorshift) }, + { 4, typeof(MersenneTwister) }, + }; + + public static IRandom Instance { get; private set; } + public static void SetInstance(IRandom instance) + { + if (instance != null) + Instance = instance; + } + + public static void SetInstanceById(int id) + { + if (randomTypes.TryGetValue(id, out var type)) + { + // 現在のインスタンスがnull または 現在のインスタンスの型が指定typeと一致しない + if (Instance == null || Instance.GetType() != type) + { + Instance = Activator.CreateInstance(type) as IRandom ?? Instance; + } + } + else Logger.Warn($"無効なID: {id}", "IRandom.SetInstanceById"); + } +} \ No newline at end of file diff --git a/Modules/Random/MersenneTwister.cs b/Modules/Random/MersenneTwister.cs new file mode 100644 index 000000000..f42343909 --- /dev/null +++ b/Modules/Random/MersenneTwister.cs @@ -0,0 +1,132 @@ +// Copyright(c) 2015 vpmedia +// Released under the MIT license + +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; + +namespace TOHE; + +public class MersenneTwister : IRandom +{ + // 参考元 + public const string REFERENCE_HOMEPAGE = "http://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/mt.html"; + public const string REFERENCE_SOURCE_CODE = "https://github.com/vpmedia/template-unity/blob/master/Framework/Assets/Frameworks/URandom/MersenneTwister.cs"; + + public MersenneTwister() : this((Int32)DateTime.UtcNow.Ticks) { } + public MersenneTwister(Int32 seed) + { + Init((UInt32)seed); + } + + /// + /// 数値の上限を設定 + /// これより下の値の一部は参考元のソースより拝借 + /// + private const Int32 N = 624; + private const Int32 M = 397; + private const UInt32 MatrixA = 0x9908b0df; + private const UInt32 UpperMask = 0x80000000; + private const UInt32 LowerMask = 0x7fffffff; + private const UInt32 TemperingMaskB = 0x9d2c5680; + private const UInt32 TemperingMaskC = 0xefc60000; + + private static UInt32 ShiftU(UInt32 y) + { + return y >> 11; + } + + private static UInt32 ShiftS(UInt32 y) + { + return y << 7; + } + + private static UInt32 ShiftT(UInt32 y) + { + return y << 15; + } + + private static UInt32 ShiftL(UInt32 y) + { + return y >> 18; + } + + private readonly UInt32[] _mt = new UInt32[N]; + private Int16 _mtItems; + private readonly UInt32[] _mag01 = { 0x0, MatrixA }; + + private void Init(UInt32 seed) + { + _mt[0] = seed & 0xffffffffU; + + for (_mtItems = 1; _mtItems < N; _mtItems++) + { + _mt[_mtItems] = (uint)(1812433253U * (_mt[_mtItems - 1] ^ (_mt[_mtItems - 1] >> 30)) + _mtItems); + _mt[_mtItems] &= 0xffffffffU; + } + } + + public uint Next() + { + UInt32 y; + + /* _mag01[x] = x * MatrixA for x=0,1 */ + if (_mtItems >= N) /* generate N words at one time */ + { + Int16 kk = 0; + + for (; kk < N - M; ++kk) + { + y = (_mt[kk] & UpperMask) | (_mt[kk + 1] & LowerMask); + _mt[kk] = _mt[kk + M] ^ (y >> 1) ^ _mag01[y & 0x1]; + } + + for (; kk < N - 1; ++kk) + { + y = (_mt[kk] & UpperMask) | (_mt[kk + 1] & LowerMask); + _mt[kk] = _mt[kk + (M - N)] ^ (y >> 1) ^ _mag01[y & 0x1]; + } + + y = (_mt[N - 1] & UpperMask) | (_mt[0] & LowerMask); + _mt[N - 1] = _mt[M - 1] ^ (y >> 1) ^ _mag01[y & 0x1]; + + _mtItems = 0; + } + + y = _mt[_mtItems++]; + y ^= ShiftU(y); + y ^= ShiftS(y) & TemperingMaskB; + y ^= ShiftT(y) & TemperingMaskC; + y ^= ShiftL(y); + + return y; + } + + public int Next(int minValue, int maxValue) + { + if (minValue < 0 || maxValue < 0) throw new ArgumentOutOfRangeException("minValue and maxValue must be bigger than 0."); + else if (minValue > maxValue) throw new ArgumentException("maxValue must be bigger than minValue."); + else if (minValue == maxValue) return minValue; + + return (int)(minValue + (Next() % (maxValue - minValue))); + } + + public int Next(int maxValue) => Next(0, maxValue); +} \ No newline at end of file diff --git a/Modules/Random/NetRandomWrapper.cs b/Modules/Random/NetRandomWrapper.cs new file mode 100644 index 000000000..9ad46a116 --- /dev/null +++ b/Modules/Random/NetRandomWrapper.cs @@ -0,0 +1,21 @@ +using System; + +namespace TOHE; + +public class NetRandomWrapper : IRandom +{ + public Random wrapping; + + public NetRandomWrapper() : this(new Random()) + { } + public NetRandomWrapper(int seed) : this(new Random(seed)) + { } + public NetRandomWrapper(Random instance) + { + wrapping = instance; + } + + public int Next(int minValue, int maxValue) => wrapping.Next(minValue, maxValue); + public int Next(int maxValue) => wrapping.Next(maxValue); + public int Next() => wrapping.Next(); +} \ No newline at end of file diff --git a/Modules/Random/Xorshift.cs b/Modules/Random/Xorshift.cs new file mode 100644 index 000000000..8ea2ccc28 --- /dev/null +++ b/Modules/Random/Xorshift.cs @@ -0,0 +1,36 @@ +using System; + +namespace TOHE; + +public class Xorshift : IRandom +{ + // 参考元 + public const string REFERENCE = "https://ja.wikipedia.org/wiki/Xorshift"; + + private uint num; + + public Xorshift() : this((uint)DateTime.UtcNow.Ticks) + { } + public Xorshift(uint seed) + { + num = seed; + } + + public uint Next() + { + num ^= num << 13; + num ^= num >> 17; + num ^= num << 5; + + return num; + } + public int Next(int minValue, int maxValue) + { + if (minValue < 0 || maxValue < 0) throw new ArgumentOutOfRangeException("minValue and maxValue must be bigger than 0."); + else if (minValue > maxValue) throw new ArgumentException("maxValue must be bigger than minValue."); + else if (minValue == maxValue) return minValue; + + return (int)(minValue + (Next() % (maxValue - minValue))); + } + public int Next(int maxValue) => Next(0, maxValue); +} \ No newline at end of file diff --git a/Modules/RetributionistRevengeManager.cs b/Modules/RetributionistRevengeManager.cs new file mode 100644 index 000000000..8cd395d11 --- /dev/null +++ b/Modules/RetributionistRevengeManager.cs @@ -0,0 +1,186 @@ +using HarmonyLib; +using Hazel; +using Rewired.UI.ControlMapper; +using System; +using System.Linq; +using TOHE.Modules; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +public static class RetributionistRevengeManager +{ + public static bool RetributionistMsgCheck(PlayerControl pc, string msg, bool isUI = false) + { + if (!AmongUsClient.Instance.AmHost) return false; + if (!GameStates.IsInGame || pc == null) return false; + if (!pc.Is(CustomRoles.Retributionist)) return false; + msg = msg.Trim().ToLower(); + if (msg.Length < 4 || msg[..4] != "/ret") return false; + if (Options.RetributionistCanKillNum.GetInt() < 1) + { + if (!isUI) Utils.SendMessage(GetString("RetributionistKillDisable"), pc.PlayerId); + else pc.ShowPopUp(GetString("RetributionistKillDisable")); + return true; + } + int playerCount = Main.AllAlivePlayerControls.Count(); + { + if (playerCount <= Options.MinimumPlayersAliveToRetri.GetInt()) + { + if (pc.Data.IsDead) + { + if (!isUI) Utils.SendMessage(GetString("RetributionistKillTooManyDead"), pc.PlayerId); + else pc.ShowPopUp(GetString("RetributionistKillTooManyDead")); + return true; + } + } + + } + if (Options.CanOnlyRetributeWithTasksDone.GetBool()) + { + if (!pc.GetPlayerTaskState().IsTaskFinished && pc.Data.IsDead) + { + if (!isUI) Utils.SendMessage(GetString("RetributionistKillDisable"), pc.PlayerId); + else pc.ShowPopUp(GetString("RetributionistKillDisable")); + return true; + } + } + if (!pc.Data.IsDead) + { + Utils.SendMessage(GetString("RetributionistAliveKill"), pc.PlayerId); + return true; + } + + if (msg == "/ret") + { + string text = GetString("PlayerIdList"); + foreach (var npc in Main.AllAlivePlayerControls) + text += "\n" + npc.PlayerId.ToString() + " → (" + npc.GetDisplayRoleName() + ") " + npc.GetRealName(); + Utils.SendMessage(text, pc.PlayerId); + return true; + } + + if (Main.RetributionistRevenged.ContainsKey(pc.PlayerId)) + { + if (Main.RetributionistRevenged[pc.PlayerId] >= Options.RetributionistCanKillNum.GetInt()) + { + if (!isUI) Utils.SendMessage(GetString("RetributionistKillMax"), pc.PlayerId); + else pc.ShowPopUp(GetString("RetributionistKillMax")); + return true; + } + } + else + { + Main.RetributionistRevenged.Add(pc.PlayerId, 0); + } + + int targetId; + PlayerControl target; + try + { + targetId = int.Parse(msg.Replace("/ret", string.Empty)); + target = Utils.GetPlayerById(targetId); + } + catch + { + if (!isUI) Utils.SendMessage(GetString("RetributionistKillDead"), pc.PlayerId); + else pc.ShowPopUp(GetString("RetributionistKillDead")); + return true; + } + + if (target == null || target.Data.IsDead) + { + if (!isUI) Utils.SendMessage(GetString("RetributionistKillDead"), pc.PlayerId); + else pc.ShowPopUp(GetString("RetributionistKillDead")); + return true; + } + if (target.Is(CustomRoles.Pestilence)) + { + if (!isUI) Utils.SendMessage(GetString("PestilenceImmune"), pc.PlayerId); + else pc.ShowPopUp(GetString("PestilenceImmune")); + return true; + } + + Logger.Info($"{pc.GetNameWithRole()} 复仇了 {target.GetNameWithRole()}", "Retributionist"); + + string Name = target.GetRealName(); + + Main.RetributionistRevenged[pc.PlayerId]++; + + CustomSoundsManager.RPCPlayCustomSoundAll("AWP"); + + new LateTask(() => + { + Main.PlayerStates[target.PlayerId].deathReason = PlayerState.DeathReason.Revenge; + target.SetRealKiller(pc); + + if (GameStates.IsMeeting) + { + GuessManager.RpcGuesserMurderPlayer(target); + + //死者检查 + Utils.AfterPlayerDeathTasks(target, true); + + Utils.NotifyRoles(isForMeeting: false, NoCache: true); + } + else + { + target.RpcMurderPlayerV3(target); + Main.PlayerStates[target.PlayerId].SetDead(); + } + + new LateTask(() => { Utils.SendMessage(string.Format(GetString("RetributionistKillSucceed"), Name), 255, Utils.ColorString(Utils.GetRoleColor(CustomRoles.Retributionist), GetString("RetributionistRevengeTitle"))); }, 0.6f, "Retributionist Kill"); + + }, 0.2f, "Retributionist Kill"); + return true; + } + + private static void SendRPC(byte playerId) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.RetributionistRevenge, SendOption.Reliable, -1); + writer.Write(playerId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static void ReceiveRPC(MessageReader reader, PlayerControl pc) + { + int PlayerId = reader.ReadByte(); + RetributionistMsgCheck(pc, $"/ret {PlayerId}", true); + } + + private static void RetributionistOnClick(byte playerId, MeetingHud __instance) + { + Logger.Msg($"Click: ID {playerId}", "Retributionist UI"); + var pc = Utils.GetPlayerById(playerId); + if (pc == null || !pc.IsAlive() || !GameStates.IsVoting) return; + if (AmongUsClient.Instance.AmHost) RetributionistMsgCheck(PlayerControl.LocalPlayer, $"/ret {playerId}", true); + else SendRPC(playerId); + } + + [HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))] + class StartMeetingPatch + { + public static void Postfix(MeetingHud __instance) + { + if (PlayerControl.LocalPlayer.Is(CustomRoles.Retributionist) && !PlayerControl.LocalPlayer.IsAlive()) + CreateJudgeButton(__instance); + } + } + public static void CreateJudgeButton(MeetingHud __instance) + { + foreach (var pva in __instance.playerStates) + { + var pc = Utils.GetPlayerById(pva.TargetPlayerId); + if (pc == null || !pc.IsAlive()) continue; + GameObject template = pva.Buttons.transform.Find("CancelButton").gameObject; + GameObject targetBox = UnityEngine.Object.Instantiate(template, pva.transform); + targetBox.name = "ShootButton"; + targetBox.transform.localPosition = new Vector3(-0.95f, 0.03f, -1.31f); + SpriteRenderer renderer = targetBox.GetComponent(); + renderer.sprite = CustomButton.Get("TargetIcon"); + PassiveButton button = targetBox.GetComponent(); + button.OnClick.RemoveAllListeners(); + button.OnClick.AddListener((Action)(() => RetributionistOnClick(pva.TargetPlayerId, __instance))); + } + } +} diff --git a/Modules/ServerAddManager.cs b/Modules/ServerAddManager.cs new file mode 100644 index 000000000..5d0be3d51 --- /dev/null +++ b/Modules/ServerAddManager.cs @@ -0,0 +1,61 @@ +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; + +namespace TOHE; + +public static class ServerAddManager +{ + private static ServerManager serverManager = DestroyableSingleton.Instance; + public static void Init() + { + if (CultureInfo.CurrentCulture.Name.StartsWith("zh") && serverManager.AvailableRegions.Count == 7) return; + if (!CultureInfo.CurrentCulture.Name.StartsWith("zh") && serverManager.AvailableRegions.Count == 6) return; + + serverManager.AvailableRegions = ServerManager.DefaultRegions; + List regionInfos = new(); + + if (CultureInfo.CurrentCulture.Name.StartsWith("zh")) + { + regionInfos.Add(CreateHttp("au-sh.pafyx.top", "梦服上海 (新)", 22000, false)); + //regionInfos.Add(CreateHttp("120.78.171.61", "霸总广州", 22000, false)); + } + regionInfos.Add(CreateHttp("au-as.duikbo.at", "Modded Asia (MAS)", 443, true)); + regionInfos.Add(CreateHttp("www.aumods.xyz", "Modded NA (MNA)", 443, true)); + regionInfos.Add(CreateHttp("au-eu.duikbo.at", "Modded EU (MEU)", 443, true)); + + regionInfos.Where(x => !serverManager.AvailableRegions.Contains(x)).Do(serverManager.AddOrUpdateRegion); + } + + public static IRegionInfo CreateHttp(string ip, string name, ushort port, bool ishttps) + { + string serverIp = (ishttps ? "https://" : "http://") + ip; + ServerInfo serverInfo = new ServerInfo(name, serverIp, port, false); + ServerInfo[] ServerInfo = new ServerInfo[] { serverInfo }; + return new StaticHttpRegionInfo(name, (StringNames)1003, ip, ServerInfo).CastFast(); + } + + private static class CastHelper where T : Il2CppObjectBase + { + public static Func Cast; + static CastHelper() + { + var constructor = typeof(T).GetConstructor(new[] { typeof(IntPtr) }); + var ptr = Expression.Parameter(typeof(IntPtr)); + var create = Expression.New(constructor!, ptr); + var lambda = Expression.Lambda>(create, ptr); + Cast = lambda.Compile(); + } + } + + private static T CastFast(this Il2CppObjectBase obj) where T : Il2CppObjectBase + { + if (obj is T casted) return casted; + return CastHelper.Cast(obj.Pointer); + } + +} \ No newline at end of file diff --git a/Modules/SpamManager.cs b/Modules/SpamManager.cs new file mode 100644 index 000000000..3c6af7072 --- /dev/null +++ b/Modules/SpamManager.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using static TOHE.Translator; + +namespace TOHE; + +public static class SpamManager +{ + private static readonly string BANEDWORDS_FILE_PATH = "./TOHE_DATA/BanWords.txt"; + public static List BanWords = new(); + public static void Init() + { + CreateIfNotExists(); + BanWords = ReturnAllNewLinesInFile(BANEDWORDS_FILE_PATH); + } + public static void CreateIfNotExists() + { + if (!File.Exists(BANEDWORDS_FILE_PATH)) + { + try + { + if (!Directory.Exists(@"TOHE_DATA")) Directory.CreateDirectory(@"TOHE_DATA"); + if (File.Exists(@"./BanWords.txt")) File.Move(@"./BanWords.txt", BANEDWORDS_FILE_PATH); + else + { + string fileName; + string[] name = CultureInfo.CurrentCulture.Name.Split("-"); + if (name.Count() >= 2) + fileName = name[0] switch + { + "zh" => "SChinese", + "ru" => "Russian", + _ => "English" + }; + else fileName = "English"; + Logger.Warn($"创建新的 BanWords 文件:{fileName}", "SpamManager"); + File.WriteAllText(BANEDWORDS_FILE_PATH, GetResourcesTxt($"TOHE.Resources.Config.BanWords.{fileName}.txt")); + } + } + catch (Exception ex) + { + Logger.Exception(ex, "SpamManager"); + } + } + } + private static string GetResourcesTxt(string path) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + stream.Position = 0; + using StreamReader reader = new(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + public static List ReturnAllNewLinesInFile(string filename) + { + if (!File.Exists(filename)) return new List(); + using StreamReader sr = new(filename, Encoding.GetEncoding("UTF-8")); + string text; + List sendList = new(); + while ((text = sr.ReadLine()) != null) + if (text.Length > 1 && text != "") sendList.Add(text.Replace("\\n", "\n").ToLower()); + return sendList; + } + public static bool CheckSpam(PlayerControl player, string text) + { + if (player.PlayerId == PlayerControl.LocalPlayer.PlayerId) return false; + string name = player.GetRealName(); + bool kick = false; + string msg = ""; + + if (Options.AutoKickStart.GetBool()) + { + if (ContainsStart(text) && GameStates.IsLobby) + { + msg = string.Format(GetString("Message.KickWhoSayStart"), name); + if (Options.AutoKickStart.GetBool()) + { + if (!Main.SayStartTimes.ContainsKey(player.GetClientId())) Main.SayStartTimes.Add(player.GetClientId(), 0); + Main.SayStartTimes[player.GetClientId()]++; + msg = string.Format(GetString("Message.WarnWhoSayStart"), name, Main.SayStartTimes[player.GetClientId()]); + if (Main.SayStartTimes[player.GetClientId()] > Options.AutoKickStartTimes.GetInt()) + { + msg = string.Format(GetString("Message.KickStartAfterWarn"), name, Main.SayStartTimes[player.GetClientId()]); + kick = true; + } + } + if (msg != "") Utils.SendMessage(msg); + if (kick) AmongUsClient.Instance.KickPlayer(player.GetClientId(), Options.AutoKickStartAsBan.GetBool()); + return true; + } + } + + bool banned = BanWords.Any(text.Contains); + + if (!banned) return false; + + if (Options.AutoWarnStopWords.GetBool()) msg = string.Format(GetString("Message.WarnWhoSayBanWord"), name); + if (Options.AutoKickStopWords.GetBool()) + { + if (!Main.SayBanwordsTimes.ContainsKey(player.GetClientId())) Main.SayBanwordsTimes.Add(player.GetClientId(), 0); + Main.SayBanwordsTimes[player.GetClientId()]++; + msg = string.Format(GetString("Message.WarnWhoSayBanWordTimes"), name, Main.SayBanwordsTimes[player.GetClientId()]); + if (Main.SayBanwordsTimes[player.GetClientId()] > Options.AutoKickStopWordsTimes.GetInt()) + { + msg = string.Format(GetString("Message.KickWhoSayBanWordAfterWarn"), name, Main.SayBanwordsTimes[player.GetClientId()]); + kick = true; + } + } + + if (msg != "") + { + if (kick || !GameStates.IsInGame) Utils.SendMessage(msg); + else + { + foreach (var pc in Main.AllPlayerControls.Where(x => x.IsAlive() == player.IsAlive())) + Utils.SendMessage(msg, pc.PlayerId); + } + } + if (kick) AmongUsClient.Instance.KickPlayer(player.GetClientId(), Options.AutoKickStopWordsAsBan.GetBool()); + return true; + } + private static bool ContainsStart(string text) + { + text = text.Trim().ToLower(); + + int stNum = 0; + for (int i = 0; i < text.Length; i++) + { + if (text[i..].Equals("k")) stNum++; + if (text[i..].Equals("开")) stNum++; + } + if (stNum >= 3) return true; + + if (text == "Start") return true; + if (text == "start") return true; + if (text == "/Start") return true; + if (text == "/Start/") return true; + if (text == "Start/") return true; + if (text == "/start") return true; + if (text == "/start/") return true; + if (text == "start/") return true; + + if (text == "plsstart") return true; + if (text == "pls start") return true; + if (text == "please start") return true; + if (text == "pleasestart") return true; + + if (text == "Plsstart") return true; + if (text == "Pls start") return true; + if (text == "Please start") return true; + if (text == "Pleasestart") return true; + + if (text == "plsStart") return true; + if (text == "pls Start") return true; + if (text == "please Start") return true; + if (text == "pleaseStart") return true; + + if (text == "PlsStart") return true; + if (text == "Pls Start") return true; + if (text == "Please Start") return true; + if (text == "PleaseStart") return true; + + if (text == "sTart") return true; + if (text == "stArt") return true; + if (text == "staRt") return true; + if (text == "starT") return true; + + if (text == "s t a r t") return true; + if (text == "S t a r t") return true; + + if (text == "started") return true; + if (text == "Started") return true; + if (text == "s t a r t e d") return true; + if (text == "S t a r t e d") return true; + + if (text == "Го") return true; + if (text == "гО") return true; + if (text == "го") return true; + if (text == "Гоу") return true; + if (text == "гоу") return true; + if (text == "Старт") return true; + if (text == "старт") return true; + if (text == "/Старт") return true; + if (text == "/Старт/") return true; + if (text == "Старт/") return true; + if (text == "/старт") return true; + if (text == "/старт/") return true; + if (text == "старт/") return true; + + if (text == "пжстарт") return true; + if (text == "пж старт") return true; + if (text == "пжСтарт") return true; + if (text == "пж Старт") return true; + if (text == "Пжстарт") return true; + if (text == "Пж старт") return true; + if (text == "ПжСтарт") return true; + if (text == "Пж Старт") return true; + + if (text == "сТарт") return true; + if (text == "стАрт") return true; + if (text == "стаРт") return true; + if (text == "старТ") return true; + + if (text == "с т а р т") return true; + if (text == "С т а р т") return true; + + if (text == "начни") return true; + if (text == "Начни") return true; + if (text == "начинай") return true; + if (text == "начинай уже") return true; + if (text == "Начинай") return true; + if (text == "Начинай уже") return true; + if (text == "Начинай Уже") return true; + if (text == "н а ч и н а й") return true; + if (text == "Н а ч и н а й") return true; + + if (text == "пж го") return true; + if (text == "пжго") return true; + if (text == "Пж Го") return true; + if (text == "Пж го") return true; + if (text == "пж Го") return true; + if (text == "ПжГо") return true; + if (text == "Пжго") return true; + if (text == "пжГо") return true; + if (text == "ГоПж") return true; + if (text == "гоПж") return true; + if (text == "Гопж") return true; + + if (text == "开") return true; + if (text == "快开") return true; + if (text == "开始") return true; + if (text == "开啊") return true; + if (text == "开阿") return true; + if (text == "kai") return true; + if (text == "kaishi") return true; + + //if (text.Length >= 3) return false; + if (text.Contains("start")) return true; + if (text.Contains("s t a r t")) return true; + if (text.Contains("begin")) return true; + + if (text.Contains("了")) return false; + if (text.Contains("没")) return false; + if (text.Contains("吗")) return false; + if (text.Contains("哈")) return false; + if (text.Contains("还")) return false; + if (text.Contains("现")) return false; + if (text.Contains("不")) return false; + if (text.Contains("可")) return false; + if (text.Contains("刚")) return false; + if (text.Contains("的")) return false; + if (text.Contains("打")) return false; + if (text.Contains("门")) return false; + if (text.Contains("关")) return false; + if (text.Contains("怎")) return false; + if (text.Contains("要")) return false; + if (text.Contains("摆")) return false; + if (text.Contains("啦")) return false; + if (text.Contains("咯")) return false; + if (text.Contains("嘞")) return false; + if (text.Contains("勒")) return false; + if (text.Contains("心")) return false; + if (text.Contains("呢")) return false; + if (text.Contains("门")) return false; + if (text.Contains("总")) return false; + if (text.Contains("哥")) return false; + if (text.Contains("姐")) return false; + if (text.Contains("《")) return false; + if (text.Contains("?")) return false; + if (text.Contains("?")) return false; + + return text.Contains("开") || text.Contains("kai"); + } +} diff --git a/Modules/TargetArrow.cs b/Modules/TargetArrow.cs new file mode 100644 index 000000000..9581c19ec --- /dev/null +++ b/Modules/TargetArrow.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace TOHE; + +static class TargetArrow +{ + class ArrowInfo + { + public byte From; + public byte To; + public ArrowInfo(byte from, byte to) + { + From = from; + To = to; + } + public bool Equals(ArrowInfo obj) + { + return From == obj.From && To == obj.To; + } + public override string ToString() + { + return $"(From:{From} To:{To})"; + } + } + + static Dictionary TargetArrows = new(); + static readonly string[] Arrows = { + "↑", + "↗", + "→", + "↘", + "↓", + "↙", + "←", + "↖", + "・" + }; + + public static void Init() + { + TargetArrows.Clear(); + } + /// + /// 新たにターゲット矢印対象を登録 + /// + /// + /// + /// + public static void Add(byte seer, byte target) + { + var arrowInfo = new ArrowInfo(seer, target); + if (!TargetArrows.Any(a => a.Key.Equals(arrowInfo))) + TargetArrows[arrowInfo] = "・"; + } + /// + /// ターゲットの削除 + /// + /// + /// + public static void Remove(byte seer, byte target) + { + var arrowInfo = new ArrowInfo(seer, target); + var removeList = new List(TargetArrows.Keys.Where(k => k.Equals(arrowInfo))); + foreach (var a in removeList) + { + TargetArrows.Remove(a); + } + } + /// + /// タイプの同じターゲットの全削除 + /// + /// + public static void RemoveAllTarget(byte seer) + { + var removeList = new List(TargetArrows.Keys.Where(k => k.From == seer)); + foreach (var arrowInfo in removeList) + { + TargetArrows.Remove(arrowInfo); + } + } + /// + /// 見ることのできるすべてのターゲット矢印を取得 + /// + /// + /// + public static string GetArrows(PlayerControl seer, params byte[] targets) + { + var arrows = ""; + foreach (var arrowInfo in TargetArrows.Keys.Where(ai => ai.From == seer.PlayerId && targets.Contains(ai.To))) + { + arrows += TargetArrows[arrowInfo]; + } + return arrows; + } + /// + /// FixedUpdate毎にターゲット矢印を確認 + /// 更新があったらNotifyRolesを発行 + /// + /// + public static void OnFixedUpdate(PlayerControl seer) + { + if (!GameStates.IsInTask) return; + + var seerId = seer.PlayerId; + var seerIsDead = !seer.IsAlive(); + + var arrowList = new List(TargetArrows.Keys.Where(a => a.From == seer.PlayerId)); + if (arrowList.Count == 0) return; + + var update = false; + foreach (var arrowInfo in arrowList) + { + var targetId = arrowInfo.To; + var target = Utils.GetPlayerById(targetId); + if (seerIsDead || !target.IsAlive() && !seer.Is(CustomRoles.Spiritualist)) + { + TargetArrows.Remove(arrowInfo); + update = true; + continue; + } + //対象の方角ベクトルを取る + var dir = target.transform.position - seer.transform.position; + int index; + if (dir.magnitude < 2) + { + //近い時はドット表示 + index = 8; + } + else + { + //-22.5~22.5度を0とするindexに変換 + // 下が0度、左側が+180まで右側が-180まで + // 180度足すことで上が0度の時計回り + // 45度単位のindexにするため45/2を加算 + var angle = Vector3.SignedAngle(Vector3.down, dir, Vector3.back) + 180 + 22.5; + index = ((int)(angle / 45)) % 8; + } + var arrow = Arrows[index]; + if (TargetArrows[arrowInfo] != arrow) + { + TargetArrows[arrowInfo] = arrow; + update = true; + } + } + if (update) + { + Utils.NotifyRoles(SpecifySeer: seer); + } + } +} diff --git a/Modules/TemplateManager.cs b/Modules/TemplateManager.cs new file mode 100644 index 000000000..521304838 --- /dev/null +++ b/Modules/TemplateManager.cs @@ -0,0 +1,125 @@ +using AmongUs.Data; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using static TOHE.Translator; + +namespace TOHE; + +public static class TemplateManager +{ + private static readonly string TEMPLATE_FILE_PATH = "./TOHE_DATA/template.txt"; + private static Dictionary> _replaceDictionary = new() + { + ["RoomCode"] = () => InnerNet.GameCode.IntToGameName(AmongUsClient.Instance.GameId), + ["PlayerName"] = () => DataManager.Player.Customization.Name, + ["AmongUsVersion"] = () => UnityEngine.Application.version, + ["InternalVersion"] = () => Main.PluginVersion, + ["ModVersion"] = () => Main.PluginDisplayVersion, + ["Map"] = () => Constants.MapNames[Main.NormalOptions.MapId], + ["NumEmergencyMeetings"] = () => Main.NormalOptions.NumEmergencyMeetings.ToString(), + ["EmergencyCooldown"] = () => Main.NormalOptions.EmergencyCooldown.ToString(), + ["DiscussionTime"] = () => Main.NormalOptions.DiscussionTime.ToString(), + ["VotingTime"] = () => Main.NormalOptions.VotingTime.ToString(), + ["PlayerSpeedMod"] = () => Main.NormalOptions.PlayerSpeedMod.ToString(), + ["CrewLightMod"] = () => Main.NormalOptions.CrewLightMod.ToString(), + ["ImpostorLightMod"] = () => Main.NormalOptions.ImpostorLightMod.ToString(), + ["KillCooldown"] = () => Main.NormalOptions.KillCooldown.ToString(), + ["NumCommonTasks"] = () => Main.NormalOptions.NumCommonTasks.ToString(), + ["NumLongTasks"] = () => Main.NormalOptions.NumLongTasks.ToString(), + ["NumShortTasks"] = () => Main.NormalOptions.NumShortTasks.ToString(), + ["Date"] = () => DateTime.Now.ToShortDateString(), + ["Time"] = () => DateTime.Now.ToShortTimeString(), + + }; + + public static void Init() + { + CreateIfNotExists(); + } + + public static void CreateIfNotExists() + { + if (!File.Exists(TEMPLATE_FILE_PATH)) + { + try + { + if (!Directory.Exists(@"TOHE_DATA")) Directory.CreateDirectory(@"TOHE_DATA"); + if (File.Exists(@"./template.txt")) File.Move(@"./template.txt", TEMPLATE_FILE_PATH); + else + { + string fileName; + string[] name = CultureInfo.CurrentCulture.Name.Split("-"); + if (name.Count() >= 2) + fileName = name[0] switch + { + "zh" => "SChinese", + "ru" => "Russian", + _ => "English" + }; + else fileName = "English"; + Logger.Warn($"创建新的 Template 文件:{fileName}", "TemplateManager"); + File.WriteAllText(TEMPLATE_FILE_PATH, GetResourcesTxt($"TOHE.Resources.Config.template.{fileName}.txt")); + } + } + catch (Exception ex) + { + Logger.Exception(ex, "TemplateManager"); + } + } + else + { + var text = File.ReadAllText(TEMPLATE_FILE_PATH, Encoding.GetEncoding("UTF-8")); + File.WriteAllText(TEMPLATE_FILE_PATH, text.Replace("5PNwUaN5", "hkk2p9ggv4")); + } + } + + private static string GetResourcesTxt(string path) + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + stream.Position = 0; + using StreamReader reader = new(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + + public static void SendTemplate(string str = "", byte playerId = 0xff, bool noErr = false) + { + CreateIfNotExists(); + using StreamReader sr = new(TEMPLATE_FILE_PATH, Encoding.GetEncoding("UTF-8")); + string text; + string[] tmp = { }; + List sendList = new(); + HashSet tags = new(); + while ((text = sr.ReadLine()) != null) + { + tmp = text.Split(":"); + if (tmp.Length > 1 && tmp[1] != "") + { + tags.Add(tmp[0]); + if (tmp[0].ToLower() == str.ToLower()) sendList.Add(tmp.Skip(1).Join(delimiter: ":").Replace("\\n", "\n")); + } + } + if (sendList.Count == 0 && !noErr) + { + if (playerId == 0xff) + HudManager.Instance.Chat.AddChat(PlayerControl.LocalPlayer, string.Format(GetString("Message.TemplateNotFoundHost"), str, tags.Join(delimiter: ", "))); + else Utils.SendMessage(string.Format(GetString("Message.TemplateNotFoundClient"), str), playerId); + } + else for (int i = 0; i < sendList.Count; i++) Utils.SendMessage(ApplyReplaceDictionary(sendList[i]), playerId); + } + + private static string ApplyReplaceDictionary(string text) + { + foreach (var kvp in _replaceDictionary) + { + text = Regex.Replace(text, "{{" + kvp.Key + "}}", kvp.Value.Invoke() ?? "", RegexOptions.IgnoreCase); + } + return text; + } +} \ No newline at end of file diff --git a/Modules/Translator.cs b/Modules/Translator.cs new file mode 100644 index 000000000..0d37c116f --- /dev/null +++ b/Modules/Translator.cs @@ -0,0 +1,182 @@ +using Csv; +using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace TOHE; + +public static class Translator +{ + public static Dictionary> translateMaps; + public const string LANGUAGE_FOLDER_NAME = "Language"; + public static void Init() + { + Logger.Info("加载语言文件...", "Translator"); + LoadLangs(); + Logger.Info("加载语言文件成功", "Translator"); + } + public static void LoadLangs() + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + var stream = assembly.GetManifestResourceStream("TOHE.Resources.String.csv"); + translateMaps = new Dictionary>(); + + var options = new CsvOptions() + { + HeaderMode = HeaderMode.HeaderPresent, + AllowNewLineInEnclosedFieldValues = false, + }; + foreach (var line in CsvReader.ReadFromStream(stream, options)) + { + if (line.Values[0][0] == '#') continue; + try + { + Dictionary dic = new(); + for (int i = 1; i < line.ColumnCount; i++) + { + int id = int.Parse(line.Headers[i]); + dic[id] = line.Values[i].Replace("\\n", "\n").Replace("\\r", "\r"); + } + if (!translateMaps.TryAdd(line.Values[0], dic)) + Logger.Warn($"待翻译的 CSV 文件中存在重复项:第{line.Index}行 => \"{line.Values[0]}\"", "Translator"); + } + catch (Exception ex) + { + Logger.Warn($"翻译文件错误:第{line.Index}行 => \"{line.Values[0]}\"", "Translator"); + Logger.Warn(ex.ToString(), "Translator"); + } + } + + // カスタム翻訳ファイルの読み込み + if (!Directory.Exists(LANGUAGE_FOLDER_NAME)) Directory.CreateDirectory(LANGUAGE_FOLDER_NAME); + + // 翻訳テンプレートの作成 + CreateTemplateFile(); + foreach (var lang in Enum.GetValues(typeof(SupportedLangs))) + { + if (File.Exists(@$"./{LANGUAGE_FOLDER_NAME}/{lang}.dat")) + LoadCustomTranslation($"{lang}.dat", (SupportedLangs)lang); + } + } + + public static string GetString(string s, Dictionary replacementDic = null, bool console = false) + { + var langId = TranslationController.InstanceExists ? TranslationController.Instance.currentLanguage.languageID : SupportedLangs.English; + if (console) langId = SupportedLangs.English; + if (Main.ForceOwnLanguage.Value) langId = GetUserTrueLang(); + string str = GetString(s, langId); + if (replacementDic != null) + foreach (var rd in replacementDic) + { + str = str.Replace(rd.Key, rd.Value); + } + return str; + } + + public static string GetString(string str, SupportedLangs langId) + { + var res = $""; + try + { + if (translateMaps.TryGetValue(str, out var dic) && (!dic.TryGetValue((int)langId, out res) || res == "" || (langId is not SupportedLangs.SChinese and not SupportedLangs.TChinese && Regex.IsMatch(res, @"[\u4e00-\u9fa5]") && res == GetString(str, SupportedLangs.SChinese)))) //strに該当する&無効なlangIdかresが空 + { + if (langId == SupportedLangs.English) res = $"*{str}"; + else res = GetString(str, SupportedLangs.English); + } + if (!translateMaps.ContainsKey(str)) //translateMapsにない場合、StringNamesにあれば取得する + { + var stringNames = Enum.GetValues(typeof(StringNames)).Cast().Where(x => x.ToString() == str); + if (stringNames != null && stringNames.Count() > 0) + res = GetString(stringNames.FirstOrDefault()); + } + } + catch (Exception Ex) + { + Logger.Fatal($"Error oucured at [{str}] in String.csv", "Translator"); + Logger.Error("Here was the error:\n" + Ex.ToString(), "Translator"); + } + return res; + } + public static string GetString(StringNames stringName) + => DestroyableSingleton.Instance.GetString(stringName, new Il2CppReferenceArray(0)); + public static string GetRoleString(string str, bool forUser = true) + { + var CurrentLanguage = TranslationController.Instance.currentLanguage.languageID; + var lang = forUser ? CurrentLanguage : SupportedLangs.English; + if (Main.ForceOwnLanguageRoleName.Value) + lang = GetUserTrueLang(); + + return GetString(str, lang); + } + public static SupportedLangs GetUserTrueLang() + { + try + { + var name = CultureInfo.CurrentUICulture.Name; + if (name.StartsWith("en")) return SupportedLangs.English; + if (name.StartsWith("zh_CHT")) return SupportedLangs.TChinese; + if (name.StartsWith("zh")) return SupportedLangs.SChinese; + if (name.StartsWith("ru")) return SupportedLangs.Russian; + return TranslationController.Instance.currentLanguage.languageID; + } + catch + { + return SupportedLangs.English; + } + } + public static void LoadCustomTranslation(string filename, SupportedLangs lang) + { + string path = @$"./{LANGUAGE_FOLDER_NAME}/{filename}"; + if (File.Exists(path)) + { + Logger.Info($"加载自定义翻译文件:{filename}", "LoadCustomTranslation"); + using StreamReader sr = new(path, Encoding.GetEncoding("UTF-8")); + string text; + string[] tmp = { }; + while ((text = sr.ReadLine()) != null) + { + tmp = text.Split(":"); + if (tmp.Length > 1 && tmp[1] != "") + { + try + { + translateMaps[tmp[0]][(int)lang] = tmp.Skip(1).Join(delimiter: ":").Replace("\\n", "\n").Replace("\\r", "\r"); + } + catch (KeyNotFoundException) + { + Logger.Warn($"无效密钥:{tmp[0]}", "LoadCustomTranslation"); + } + } + } + } + else + { + Logger.Error($"找不到自定义翻译文件:{filename}", "LoadCustomTranslation"); + } + } + + private static void CreateTemplateFile() + { + var sb = new StringBuilder(); + foreach (var title in translateMaps) sb.Append($"{title.Key}:\n"); + File.WriteAllText(@$"./{LANGUAGE_FOLDER_NAME}/template.dat", sb.ToString()); + } + public static void ExportCustomTranslation() + { + LoadLangs(); + var sb = new StringBuilder(); + var lang = TranslationController.Instance.currentLanguage.languageID; + foreach (var title in translateMaps) + { + if (!title.Value.TryGetValue((int)lang, out var text)) text = ""; + sb.Append($"{title.Key}:{text.Replace("\n", "\\n").Replace("\r", "\\r")}\n"); + } + File.WriteAllText(@$"./{LANGUAGE_FOLDER_NAME}/export_{lang}.dat", sb.ToString()); + } +} \ No newline at end of file diff --git a/Modules/Utils.cs b/Modules/Utils.cs new file mode 100644 index 000000000..422b5c0e3 --- /dev/null +++ b/Modules/Utils.cs @@ -0,0 +1,2223 @@ +using AmongUs.Data; +using AmongUs.GameOptions; +using Hazel; +using Il2CppInterop.Runtime.InteropTypes; +using InnerNet; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using TOHE.Modules; +using TOHE.Roles.AddOns.Crewmate; +using TOHE.Roles.AddOns.Impostor; +using TOHE.Roles.Crewmate; +using TOHE.Roles.Impostor; +using TOHE.Roles.Neutral; +using UnityEngine; +using static TOHE.Translator; + +namespace TOHE; + +public static class Utils +{ + private static readonly DateTime timeStampStartTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + public static long GetTimeStamp(DateTime? dateTime = null) => (long)((dateTime ?? DateTime.Now).ToUniversalTime() - timeStampStartTime).TotalSeconds; + public static void ErrorEnd(string text) + { + if (AmongUsClient.Instance.AmHost) + { + Logger.Fatal($"{text} 错误,触发防黑屏措施", "Anti-black"); + ChatUpdatePatch.DoBlockChat = true; + Main.OverrideWelcomeMsg = GetString("AntiBlackOutNotifyInLobby"); + new LateTask(() => + { + Logger.SendInGame(GetString("AntiBlackOutLoggerSendInGame"), true); + }, 3f, "Anti-Black Msg SendInGame"); + new LateTask(() => + { + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Error); + GameManager.Instance.LogicFlow.CheckEndCriteria(); + RPC.ForceEndGame(CustomWinner.Error); + }, 5.5f, "Anti-Black End Game"); + } + else + { + MessageWriter writer = AmongUsClient.Instance.StartRpc(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.AntiBlackout, SendOption.Reliable); + writer.Write(text); + writer.EndMessage(); + if (Options.EndWhenPlayerBug.GetBool()) + { + new LateTask(() => + { + Logger.SendInGame(GetString("AntiBlackOutRequestHostToForceEnd"), true); + }, 3f, "Anti-Black Msg SendInGame"); + } + else + { + new LateTask(() => + { + Logger.SendInGame(GetString("AntiBlackOutHostRejectForceEnd"), true); + }, 3f, "Anti-Black Msg SendInGame"); + new LateTask(() => + { + AmongUsClient.Instance.ExitGame(DisconnectReasons.Custom); + Logger.Fatal($"{text} 错误,已断开游戏", "Anti-black"); + }, 8f, "Anti-Black Exit Game"); + } + } + } + public static void TPAll(Vector2 location) + { + foreach (PlayerControl pc in Main.AllAlivePlayerControls) + TP(pc.NetTransform, location); + } + + public static void TP(CustomNetworkTransform nt, Vector2 location) + { + location += new Vector2(0, 0.3636f); + if (AmongUsClient.Instance.AmHost) nt.SnapTo(location); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(nt.NetId, (byte)RpcCalls.SnapTo, SendOption.None); + //nt.WriteVector2(location, writer); + NetHelpers.WriteVector2(location, writer); + writer.Write(nt.lastSequenceId); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + public static ClientData GetClientById(int id) + { + try + { + var client = AmongUsClient.Instance.allClients.ToArray().Where(cd => cd.Id == id).FirstOrDefault(); + return client; + } + catch + { + return null; + } + } + public static bool IsActive(SystemTypes type) + { + //Logger.Info($"SystemTypes:{type}", "IsActive"); + int mapId = Main.NormalOptions.MapId; + switch (type) + { + case SystemTypes.Electrical: + { + var SwitchSystem = ShipStatus.Instance.Systems[type].Cast(); + return SwitchSystem != null && SwitchSystem.IsActive; + } + case SystemTypes.Reactor: + { + if (mapId == 2) return false; + else if (mapId == 4) + { + var HeliSabotageSystem = ShipStatus.Instance.Systems[type].Cast(); + return HeliSabotageSystem != null && HeliSabotageSystem.IsActive; + } + else + { + var ReactorSystemType = ShipStatus.Instance.Systems[type].Cast(); + return ReactorSystemType != null && ReactorSystemType.IsActive; + } + } + case SystemTypes.Laboratory: + { + if (mapId != 2) return false; + var ReactorSystemType = ShipStatus.Instance.Systems[type].Cast(); + return ReactorSystemType != null && ReactorSystemType.IsActive; + } + case SystemTypes.LifeSupp: + { + if (mapId is 2 or 4) return false; + var LifeSuppSystemType = ShipStatus.Instance.Systems[type].Cast(); + return LifeSuppSystemType != null && LifeSuppSystemType.IsActive; + } + case SystemTypes.Comms: + { + if (mapId == 1) + { + var HqHudSystemType = ShipStatus.Instance.Systems[type].Cast(); + return HqHudSystemType != null && HqHudSystemType.IsActive; + } + else + { + var HudOverrideSystemType = ShipStatus.Instance.Systems[type].Cast(); + return HudOverrideSystemType != null && HudOverrideSystemType.IsActive; + } + } + default: + return false; + } + } + public static void SetVision(this IGameOptions opt, bool HasImpVision) + { + if (HasImpVision) + { + opt.SetFloat( + FloatOptionNames.CrewLightMod, + opt.GetFloat(FloatOptionNames.ImpostorLightMod)); + if (IsActive(SystemTypes.Electrical)) + { + opt.SetFloat( + FloatOptionNames.CrewLightMod, + opt.GetFloat(FloatOptionNames.CrewLightMod) * 5); + } + return; + } + else + { + opt.SetFloat( + FloatOptionNames.ImpostorLightMod, + opt.GetFloat(FloatOptionNames.CrewLightMod)); + if (IsActive(SystemTypes.Electrical)) + { + opt.SetFloat( + FloatOptionNames.ImpostorLightMod, + opt.GetFloat(FloatOptionNames.ImpostorLightMod) / 5); + } + return; + } + } + //誰かが死亡したときのメソッド + public static void TargetDies(PlayerControl killer, PlayerControl target) + { + if (!target.Data.IsDead || GameStates.IsMeeting) return; + foreach (var seer in Main.AllPlayerControls) + { + if (KillFlashCheck(killer, target, seer)) + { + seer.KillFlash(); + continue; + } + else if (target.Is(CustomRoles.CyberStar)) + { + if (!Options.ImpKnowCyberStarDead.GetBool() && seer.GetCustomRole().IsImpostor()) continue; + if (!Options.NeutralKnowCyberStarDead.GetBool() && seer.GetCustomRole().IsNeutral()) continue; + seer.KillFlash(); + seer.Notify(ColorString(GetRoleColor(CustomRoles.CyberStar), GetString("OnCyberStarDead"))); + } + } + if (target.Is(CustomRoles.CyberStar) && !Main.CyberStarDead.Contains(target.PlayerId)) Main.CyberStarDead.Add(target.PlayerId); + } + public static bool KillFlashCheck(PlayerControl killer, PlayerControl target, PlayerControl seer) + { + if (seer.Is(CustomRoles.GM) || seer.Is(CustomRoles.Seer)) return true; + if (seer.Data.IsDead || killer == seer || target == seer) return false; + if (seer.Is(CustomRoles.EvilTracker)) return EvilTracker.KillFlashCheck(killer, target); + return false; + } + public static void KillFlash(this PlayerControl player) + { + //キルフラッシュ(ブラックアウト+リアクターフラッシュ)の処理 + bool ReactorCheck = false; //リアクターフラッシュの確認 + if (Main.NormalOptions.MapId == 2) ReactorCheck = IsActive(SystemTypes.Laboratory); + else ReactorCheck = IsActive(SystemTypes.Reactor); + + var Duration = Options.KillFlashDuration.GetFloat(); + if (ReactorCheck) Duration += 0.2f; //リアクター中はブラックアウトを長くする + + //実行 + Main.PlayerStates[player.PlayerId].IsBlackOut = true; //ブラックアウト + if (player.AmOwner) + { + FlashColor(new(1f, 0f, 0f, 0.3f)); + if (Constants.ShouldPlaySfx()) RPC.PlaySound(player.PlayerId, Sounds.KillSound); + } + else if (player.IsModClient()) + { + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.KillFlash, SendOption.Reliable, player.GetClientId()); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + else if (!ReactorCheck) player.ReactorFlash(0f); //リアクターフラッシュ + player.MarkDirtySettings(); + new LateTask(() => + { + Main.PlayerStates[player.PlayerId].IsBlackOut = false; //ブラックアウト解除 + player.MarkDirtySettings(); + }, Options.KillFlashDuration.GetFloat(), "RemoveKillFlash"); + } + public static void BlackOut(this IGameOptions opt, bool IsBlackOut) + { + opt.SetFloat(FloatOptionNames.ImpostorLightMod, Main.DefaultImpostorVision); + opt.SetFloat(FloatOptionNames.CrewLightMod, Main.DefaultCrewmateVision); + if (IsBlackOut) + { + opt.SetFloat(FloatOptionNames.ImpostorLightMod, 0); + opt.SetFloat(FloatOptionNames.CrewLightMod, 0); + } + return; + } + public static string GetDisplayRoleName(byte playerId, bool pure = false) + { + var TextData = GetRoleText(playerId, playerId, pure); + return ColorString(TextData.Item2, TextData.Item1); + } + public static string GetRoleName(CustomRoles role, bool forUser = true) + { + return GetRoleString(Enum.GetName(typeof(CustomRoles), role), forUser); + } + public static string GetRoleMode(CustomRoles role, bool parentheses = true) + { + if (Options.HideGameSettings.GetBool() && Main.AllPlayerControls.Count() > 1) + return string.Empty; + string mode = role.GetMode() switch + { + 0 => GetString("RoleOffNoColor"), + 1 => GetString("RoleRateNoColor"), + _ => GetString("RoleOnNoColor") + }; + return parentheses ? $"({mode})" : mode; + } + public static string GetDeathReason(PlayerState.DeathReason status) + { + return GetString("DeathReason." + Enum.GetName(typeof(PlayerState.DeathReason), status)); + } + public static Color GetRoleColor(CustomRoles role) + { + if (!Main.roleColors.TryGetValue(role, out var hexColor)) hexColor = "#ffffff"; + ColorUtility.TryParseHtmlString(hexColor, out Color c); + return c; + } + public static string GetRoleColorCode(CustomRoles role) + { + if (!Main.roleColors.TryGetValue(role, out var hexColor)) hexColor = "#ffffff"; + return hexColor; + } + public static (string, Color) GetRoleText(byte seerId, byte targetId, bool pure = false) + { + string RoleText = "Invalid Role"; + Color RoleColor; + + var seerMainRole = Main.PlayerStates[seerId].MainRole; + var seerSubRoles = Main.PlayerStates[seerId].SubRoles; + + var targetMainRole = Main.PlayerStates[targetId].MainRole; + var targetSubRoles = Main.PlayerStates[targetId].SubRoles; + + var self = seerId == targetId || Main.PlayerStates[seerId].IsDead; + + RoleText = GetRoleName(targetMainRole); + RoleColor = GetRoleColor(targetMainRole); + + if (LastImpostor.currentId == targetId) + RoleText = GetRoleString("Last-") + RoleText; + + if (Options.NameDisplayAddons.GetBool() && !pure && self) + { + if (Options.AddBracketsToAddons.GetBool()) + { + if (Options.ImpEgoistVisibalToAllies.GetBool()) + { + foreach (var subRole in targetSubRoles.Where(x => x is not CustomRoles.LastImpostor and not CustomRoles.Madmate and not CustomRoles.Charmed and not CustomRoles.Recruit and not CustomRoles.Admired and not CustomRoles.Soulless and not CustomRoles.Lovers and not CustomRoles.Infected and not CustomRoles.Contagious)) + RoleText = ColorString(GetRoleColor(subRole), GetString("PrefixB." + subRole.ToString())) + RoleText; + } + if (!Options.ImpEgoistVisibalToAllies.GetBool()) + { + foreach (var subRole in targetSubRoles.Where(x => x is not CustomRoles.LastImpostor and not CustomRoles.Madmate and not CustomRoles.Charmed and not CustomRoles.Recruit and not CustomRoles.Admired and not CustomRoles.Soulless and not CustomRoles.Lovers and not CustomRoles.Infected and not CustomRoles.Contagious)) + RoleText = ColorString(GetRoleColor(subRole), GetString("PrefixB." + subRole.ToString())) + RoleText; + } + } + else if (!Options.AddBracketsToAddons.GetBool()) + { + if (Options.ImpEgoistVisibalToAllies.GetBool()) + { + foreach (var subRole in targetSubRoles.Where(x => x is not CustomRoles.LastImpostor and not CustomRoles.Madmate and not CustomRoles.Charmed and not CustomRoles.Recruit and not CustomRoles.Admired and not CustomRoles.Soulless and not CustomRoles.Lovers and not CustomRoles.Infected and not CustomRoles.Contagious)) + RoleText = ColorString(GetRoleColor(subRole), GetString("Prefix." + subRole.ToString())) + RoleText; + } + if (!Options.ImpEgoistVisibalToAllies.GetBool()) + { + foreach (var subRole in targetSubRoles.Where(x => x is not CustomRoles.LastImpostor and not CustomRoles.Madmate and not CustomRoles.Charmed and not CustomRoles.Recruit and not CustomRoles.Admired and not CustomRoles.Soulless and not CustomRoles.Lovers and not CustomRoles.Infected and not CustomRoles.Contagious)) + RoleText = ColorString(GetRoleColor(subRole), GetString("Prefix." + subRole.ToString())) + RoleText; + } + } + } + + if (targetSubRoles.Contains(CustomRoles.Madmate)) + { + RoleColor = GetRoleColor(CustomRoles.Madmate); + RoleText = GetRoleString("Mad-") + RoleText; + } + if (targetSubRoles.Contains(CustomRoles.Recruit)) + { + RoleColor = GetRoleColor(CustomRoles.Recruit); + RoleText = GetRoleString("Recruit-") + RoleText; + } + if (targetSubRoles.Contains(CustomRoles.Charmed) && (self || pure || seerMainRole == CustomRoles.Succubus || (Succubus.TargetKnowOtherTarget.GetBool() && seerSubRoles.Contains(CustomRoles.Charmed)))) + { + RoleColor = GetRoleColor(CustomRoles.Charmed); + RoleText = GetRoleString("Charmed-") + RoleText; + } + if (targetSubRoles.Contains(CustomRoles.Soulless)) + { + RoleColor = GetRoleColor(CustomRoles.Soulless); + RoleText = GetRoleString("Soulless-") + RoleText; + } + if (targetSubRoles.Contains(CustomRoles.Infected) && (self || pure || seerMainRole == CustomRoles.Infectious || (Infectious.TargetKnowOtherTarget.GetBool() && seerSubRoles.Contains(CustomRoles.Infected)))) + { + RoleColor = GetRoleColor(CustomRoles.Infected); + RoleText = GetRoleString("Infected-") + RoleText; + } + if (targetSubRoles.Contains(CustomRoles.Contagious) && (self || pure || seerMainRole == CustomRoles.Virus || (Virus.TargetKnowOtherTarget.GetBool() && seerSubRoles.Contains(CustomRoles.Contagious)))) + { + RoleColor = GetRoleColor(CustomRoles.Contagious); + RoleText = GetRoleString("Contagious-") + RoleText; + } + if (targetSubRoles.Contains(CustomRoles.Admired)) + { + RoleColor = GetRoleColor(CustomRoles.Admired); + RoleText = GetRoleString("Admired-") + RoleText; + } + + return (RoleText, RoleColor); + } + public static string GetKillCountText(byte playerId) + { + int count = Main.PlayerStates.Count(x => x.Value.GetRealKiller() == playerId); + if (count < 1) return ""; + return ColorString(new Color32(255, 69, 0, byte.MaxValue), string.Format(GetString("KillCount"), count)); + } + public static string GetVitalText(byte playerId, bool RealKillerColor = false) + { + var state = Main.PlayerStates[playerId]; + string deathReason = state.IsDead ? GetString("DeathReason." + state.deathReason) : GetString("Alive"); + if (RealKillerColor) + { + var KillerId = state.GetRealKiller(); + Color color = KillerId != byte.MaxValue ? Main.PlayerColors[KillerId] : GetRoleColor(CustomRoles.Doctor); + if (state.deathReason == PlayerState.DeathReason.Disconnected) color = new Color(255, 255, 255, 50); + deathReason = ColorString(color, deathReason); + } + return deathReason; + } + + public static bool HasTasks(GameData.PlayerInfo p, bool ForRecompute = true) + { + if (GameStates.IsLobby) return false; + //Tasksがnullの場合があるのでその場合タスク無しとする + if (p.Tasks == null) return false; + if (p.Role == null) return false; + + var hasTasks = true; + var States = Main.PlayerStates[p.PlayerId]; + if (p.Disconnected) return false; + if (p.Role.IsImpostor) + hasTasks = false; //タスクはCustomRoleを元に判定する + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) return false; + if (p.IsDead && Options.GhostIgnoreTasks.GetBool()) hasTasks = false; + var role = States.MainRole; + switch (role) + { + case CustomRoles.GM: + case CustomRoles.Sheriff: + case CustomRoles.Arsonist: + case CustomRoles.Jackal: + case CustomRoles.Sidekick: + case CustomRoles.Poisoner: + case CustomRoles.NSerialKiller: + case CustomRoles.Traitor: + case CustomRoles.Glitch: + case CustomRoles.Pickpocket: + case CustomRoles.Maverick: + case CustomRoles.Jinx: + case CustomRoles.Parasite: + case CustomRoles.Crusader: + case CustomRoles.Refugee: + case CustomRoles.Jester: + case CustomRoles.Pirate: + // case CustomRoles.Baker: + case CustomRoles.Famine: + case CustomRoles.NWitch: + case CustomRoles.Mario: + case CustomRoles.Vulture: + case CustomRoles.God: + case CustomRoles.SwordsMan: + case CustomRoles.Innocent: + case CustomRoles.Pelican: + case CustomRoles.Medusa: + case CustomRoles.Revolutionist: + case CustomRoles.FFF: + case CustomRoles.Gamer: + case CustomRoles.HexMaster: + case CustomRoles.Wraith: + // case CustomRoles.Chameleon: + case CustomRoles.Juggernaut: + case CustomRoles.Reverie: + case CustomRoles.Ritualist: + case CustomRoles.DarkHide: + case CustomRoles.Collector: + case CustomRoles.ImperiusCurse: + case CustomRoles.Provocateur: + case CustomRoles.Medic: + case CustomRoles.BloodKnight: + case CustomRoles.Camouflager: + case CustomRoles.Totocalcio: + case CustomRoles.Succubus: + case CustomRoles.CursedSoul: + case CustomRoles.Admirer: + case CustomRoles.Amnesiac: + case CustomRoles.Infectious: + case CustomRoles.Monarch: + case CustomRoles.Deputy: + case CustomRoles.Virus: + case CustomRoles.Farseer: + case CustomRoles.Counterfeiter: + case CustomRoles.Pursuer: + case CustomRoles.Spiritcaller: + case CustomRoles.PlagueBearer: + case CustomRoles.Pestilence: + case CustomRoles.Masochist: + case CustomRoles.Doomsayer: + hasTasks = false; + break; + case CustomRoles.Workaholic: + case CustomRoles.Terrorist: + case CustomRoles.Sunnyboy: + case CustomRoles.Convict: + case CustomRoles.Opportunist: + case CustomRoles.Phantom: + case CustomRoles.Baker: + // case CustomRoles.Famine: + if (ForRecompute) + hasTasks = false; + break; + case CustomRoles.Crewpostor: + if (ForRecompute && !p.IsDead) + hasTasks = false; + if (p.IsDead) + hasTasks = false; + break; + case CustomRoles.Executioner: + if (Executioner.ChangeRolesAfterTargetKilled.GetValue() != 1) + hasTasks = !ForRecompute; + else hasTasks = false; + break; + case CustomRoles.Lawyer: + if (Lawyer.ChangeRolesAfterTargetKilled.GetValue() != 1) + hasTasks = !ForRecompute; + else hasTasks = false; + break; + default: + if (role.IsImpostor()) hasTasks = false; + break; + } + + foreach (var subRole in States.SubRoles) + switch (subRole) + { + case CustomRoles.Madmate: + case CustomRoles.Charmed: + case CustomRoles.Recruit: + case CustomRoles.Egoist: + case CustomRoles.Infected: + case CustomRoles.EvilSpirit: + case CustomRoles.Contagious: + case CustomRoles.Soulless: + case CustomRoles.Rascal: + //ラバーズはタスクを勝利用にカウントしない + hasTasks &= !ForRecompute; + break; + } + if (CopyCat.playerIdList.Contains(p.PlayerId) && ForRecompute) hasTasks = false; + + return hasTasks; + } + + public static bool CanBeMadmate(this PlayerControl pc) + { + return pc != null && pc.GetCustomRole().IsCrewmate() && !pc.Is(CustomRoles.Madmate) + && !( + (pc.Is(CustomRoles.Sheriff) && !Options.SheriffCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.Mayor) && !Options.MayorCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.NiceGuesser) && !Options.NGuesserCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.Snitch) && !Options.SnitchCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.Judge) && !Options.JudgeCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.Marshall) && !Options.MarshallCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.Farseer) && !Options.FarseerCanBeMadmate.GetBool()) || + (pc.Is(CustomRoles.Retributionist) && !Options.RetributionistCanBeMadmate.GetBool()) || + pc.Is(CustomRoles.Needy) || + pc.Is(CustomRoles.Lazy) || + pc.Is(CustomRoles.Loyal) || + pc.Is(CustomRoles.SuperStar) || + pc.Is(CustomRoles.CyberStar) || + pc.Is(CustomRoles.Egoist) || + pc.Is(CustomRoles.DualPersonality) + ); + } + public static string GetProgressText(PlayerControl pc) + { + if (!Main.playerVersion.ContainsKey(0)) return ""; //ホストがMODを入れていなければ未記入を返す + var taskState = pc.GetPlayerTaskState(); + var Comms = false; + if (taskState.hasTasks) + { + if (IsActive(SystemTypes.Comms)) Comms = true; + if (Camouflager.IsActive) Comms = true; + //if (PlayerControl.LocalPlayer.myTasks.ToArray().Any(x => x.TaskType == TaskTypes.FixComms)) Comms = true; + } + return GetProgressText(pc.PlayerId, Comms); + } + public static string GetProgressText(byte playerId, bool comms = false) + { + if (!Main.playerVersion.ContainsKey(0)) return ""; //ホストがMODを入れていなければ未記入を返す + var ProgressText = new StringBuilder(); + var role = Main.PlayerStates[playerId].MainRole; + switch (role) + { + case CustomRoles.Arsonist: + var doused = GetDousedPlayerCount(playerId); + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Arsonist).ShadeColor(0.25f), $"({doused.Item1}/{doused.Item2})")); + break; + case CustomRoles.Sheriff: + ProgressText.Append(Sheriff.GetShotLimit(playerId)); + break; + case CustomRoles.Pirate: + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Pirate).ShadeColor(0.25f), $"({Pirate.NumWin}/{Pirate.SuccessfulDuelsToWin.GetInt()})")); + break; + case CustomRoles.Crusader: + ProgressText.Append(Crusader.GetSkillLimit(playerId)); + break; + case CustomRoles.CopyCat: + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.CopyCat).ShadeColor(0.25f), $"({(CopyCat.MiscopyLimit.TryGetValue(playerId, out var count2) ? count2 : 0)})")); + break; + case CustomRoles.PlagueBearer: + var plagued = PlagueBearer.PlaguedPlayerCount(playerId); + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.PlagueBearer).ShadeColor(0.25f), $"({plagued.Item1}/{plagued.Item2})")); + break; + case CustomRoles.Doomsayer: + var doomsayerguess = Doomsayer.GuessedPlayerCount(playerId); + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Doomsayer).ShadeColor(0.25f), $"({doomsayerguess.Item1}/{doomsayerguess.Item2})")); + break; + + case CustomRoles.Sniper: + ProgressText.Append(Sniper.GetBulletCount(playerId)); + break; + case CustomRoles.EvilTracker: + ProgressText.Append(EvilTracker.GetMarker(playerId)); + break; + case CustomRoles.TimeThief: + ProgressText.Append(TimeThief.GetProgressText(playerId)); + break; + case CustomRoles.Mario: + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Mario).ShadeColor(0.25f), $"({(Main.MarioVentCount.TryGetValue(playerId, out var count) ? count : 0)}/{Options.MarioVentNumWin.GetInt()})")); + break; + case CustomRoles.Vulture: + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Vulture).ShadeColor(0.25f), $"({(Vulture.BodyReportCount.TryGetValue(playerId, out var count1) ? count1 : 0)}/{Vulture.NumberOfReportsToWin.GetInt()})")); + break; + case CustomRoles.Masochist: + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Masochist).ShadeColor(0.25f), $"({(Main.MasochistKillMax.TryGetValue(playerId, out var count3) ? count3 : 0)}/{Options.MasochistKillMax.GetInt()})")); + break; + case CustomRoles.QuickShooter: + ProgressText.Append(QuickShooter.GetShotLimit(playerId)); + break; + case CustomRoles.SwordsMan: + ProgressText.Append(SwordsMan.GetKillLimit(playerId)); + break; + case CustomRoles.Pelican: + ProgressText.Append(Pelican.GetProgressText(playerId)); + break; + case CustomRoles.Counterfeiter: + ProgressText.Append(Counterfeiter.GetSeelLimit(playerId)); + break; + case CustomRoles.Pursuer: + ProgressText.Append(Pursuer.GetSeelLimit(playerId)); + break; + case CustomRoles.Revolutionist: + var draw = GetDrawPlayerCount(playerId, out var _); + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Revolutionist).ShadeColor(0.25f), $"({draw.Item1}/{draw.Item2})")); + break; + case CustomRoles.Gangster: + ProgressText.Append(Gangster.GetRecruitLimit(playerId)); + break; + case CustomRoles.Medic: + ProgressText.Append(Medic.GetSkillLimit(playerId)); + break; + case CustomRoles.CursedWolf: + int SpellCount = Main.CursedWolfSpellCount[playerId]; + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.CursedWolf), $"({SpellCount})")); + break; + case CustomRoles.Jinx: + int JinxSpellCount = Main.JinxSpellCount[playerId]; + ProgressText.Append(ColorString(GetRoleColor(CustomRoles.Jinx), $"({JinxSpellCount})")); + break; + case CustomRoles.Collector: + ProgressText.Append(Collector.GetProgressText(playerId)); + break; + case CustomRoles.Eraser: + ProgressText.Append(Eraser.GetProgressText(playerId)); + break; + case CustomRoles.Hacker: + ProgressText.Append(Hacker.GetHackLimit(playerId)); + break; + case CustomRoles.KB_Normal: + ProgressText.Append(SoloKombatManager.GetDisplayScore(playerId)); + break; + case CustomRoles.Totocalcio: + ProgressText.Append(Totocalcio.GetProgressText(playerId)); + break; + case CustomRoles.Succubus: + ProgressText.Append(Succubus.GetCharmLimit()); + break; + case CustomRoles.CursedSoul: + ProgressText.Append(CursedSoul.GetCurseLimit()); + break; + case CustomRoles.Admirer: + ProgressText.Append(Admirer.GetAdmireLimit()); + break; + case CustomRoles.Infectious: + ProgressText.Append(Infectious.GetBiteLimit()); + break; + case CustomRoles.Monarch: + ProgressText.Append(Monarch.GetKnightLimit()); + break; + case CustomRoles.Deputy: + ProgressText.Append(Deputy.GetHandcuffLimit()); + break; + case CustomRoles.Virus: + ProgressText.Append(Virus.GetInfectLimit()); + break; + case CustomRoles.EvilDiviner: + ProgressText.Append(EvilDiviner.GetDivinationCount(playerId)); + break; + case CustomRoles.Ritualist: + ProgressText.Append(Ritualist.GetRitualCount(playerId)); + break; + case CustomRoles.Jackal: + if (Jackal.CanRecruitSidekick.GetBool()) + ProgressText.Append(Jackal.GetRecruitLimit(playerId)); + break; + case CustomRoles.Spiritcaller: + ProgressText.Append(Spiritcaller.GetSpiritLimit()); + break; + default: + //タスクテキスト + var taskState = Main.PlayerStates?[playerId].GetTaskState(); + if (taskState.hasTasks) + { + Color TextColor; + var info = GetPlayerInfoById(playerId); + var TaskCompleteColor = HasTasks(info) ? Color.green : GetRoleColor(role).ShadeColor(0.5f); //タスク完了後の色 + var NonCompleteColor = HasTasks(info) ? Color.yellow : Color.white; //カウントされない人外は白色 + + if (Workhorse.IsThisRole(playerId)) + NonCompleteColor = Workhorse.RoleColor; + + var NormalColor = taskState.IsTaskFinished ? TaskCompleteColor : NonCompleteColor; + if (Main.PlayerStates.TryGetValue(playerId, out var ps) && ps.MainRole == CustomRoles.Crewpostor) + NormalColor = Color.red; + + TextColor = comms ? Color.gray : NormalColor; + string Completed = comms ? "?" : $"{taskState.CompletedTasksCount}"; + ProgressText.Append(ColorString(TextColor, $"({Completed}/{taskState.AllTasksCount})")); + } + break; + } + if (ProgressText.Length != 0) + ProgressText.Insert(0, " "); //空じゃなければ空白を追加 + + return ProgressText.ToString(); + } + public static void ShowActiveSettingsHelp(byte PlayerId = byte.MaxValue) + { + SendMessage(GetString("CurrentActiveSettingsHelp") + ":", PlayerId); + + if (Options.DisableDevices.GetBool()) { SendMessage(GetString("DisableDevicesInfo"), PlayerId); } + if (Options.SyncButtonMode.GetBool()) { SendMessage(GetString("SyncButtonModeInfo"), PlayerId); } + if (Options.SabotageTimeControl.GetBool()) { SendMessage(GetString("SabotageTimeControlInfo"), PlayerId); } + if (Options.RandomMapsMode.GetBool()) { SendMessage(GetString("RandomMapsModeInfo"), PlayerId); } + if (Options.EnableGM.GetBool()) { SendMessage(GetRoleName(CustomRoles.GM) + GetString("GMInfoLong"), PlayerId); } + foreach (var role in Enum.GetValues(typeof(CustomRoles)).Cast()) + { + if (role.IsEnable() && !role.IsVanilla()) SendMessage(GetRoleName(role) + GetRoleMode(role) + GetString(Enum.GetName(typeof(CustomRoles), role) + "InfoLong"), PlayerId); + } + + if (Options.NoGameEnd.GetBool()) { SendMessage(GetString("NoGameEndInfo"), PlayerId); } + } + public static void ShowActiveSettings(byte PlayerId = byte.MaxValue) + { + if (Options.HideGameSettings.GetBool() && PlayerId != byte.MaxValue) + { + SendMessage(GetString("Message.HideGameSettings"), PlayerId); + return; + } + if (Options.DIYGameSettings.GetBool()) + { + SendMessage(GetString("Message.NowOverrideText"), PlayerId); + return; + } + + var sb = new StringBuilder(); + sb.Append(" ★ " + GetString("TabGroup.SystemSettings")); + foreach (var opt in OptionItem.AllOptions.Where(x => x.GetBool() && x.Parent == null && x.Tab is TabGroup.SystemSettings && !x.IsHiddenOn(Options.CurrentGameMode))) + { + sb.Append($"\n{opt.GetName(true)}: {opt.GetString()}"); + //ShowChildrenSettings(opt, ref sb); + var text = sb.ToString(); + sb.Clear().Append(text.RemoveHtmlTags()); + } + sb.Append("\n\n ★ " + GetString("TabGroup.GameSettings")); + foreach (var opt in OptionItem.AllOptions.Where(x => x.GetBool() && x.Parent == null && x.Tab is TabGroup.GameSettings && !x.IsHiddenOn(Options.CurrentGameMode))) + { + sb.Append($"\n{opt.GetName(true)}: {opt.GetString()}"); + //ShowChildrenSettings(opt, ref sb); + var text = sb.ToString(); + sb.Clear().Append(text.RemoveHtmlTags()); + } + + SendMessage(sb.ToString(), PlayerId); + } + + public static void ShowAllActiveSettings(byte PlayerId = byte.MaxValue) + { + var mapId = Main.NormalOptions.MapId; + if (Options.HideGameSettings.GetBool() && PlayerId != byte.MaxValue) + { + SendMessage(GetString("Message.HideGameSettings"), PlayerId); + return; + } + if (Options.DIYGameSettings.GetBool()) + { + SendMessage(GetString("Message.NowOverrideText"), PlayerId); + return; + } + var sb = new StringBuilder(); + + sb.Append(GetString("Settings")).Append(":"); + foreach (var role in Options.CustomRoleCounts) + { + if (!role.Key.IsEnable()) continue; + string mode = role.Key.GetMode() == 1 ? GetString("RoleRateNoColor") : GetString("RoleOnNoColor"); + sb.Append($"\n【{GetRoleName(role.Key)}:{mode} ×{role.Key.GetCount()}】\n"); + ShowChildrenSettings(Options.CustomRoleSpawnChances[role.Key], ref sb); + var text = sb.ToString(); + sb.Clear().Append(text.RemoveHtmlTags()); + } + foreach (var opt in OptionItem.AllOptions.Where(x => x.GetBool() && x.Parent == null && x.Id >= 80000 && !x.IsHiddenOn(Options.CurrentGameMode))) + { + if (opt.Name is "KillFlashDuration" or "RoleAssigningAlgorithm") + sb.Append($"\n【{opt.GetName(true)}: {opt.GetString()}】\n"); + else + sb.Append($"\n【{opt.GetName(true)}】\n"); + ShowChildrenSettings(opt, ref sb); + var text = sb.ToString(); + sb.Clear().Append(text.RemoveHtmlTags()); + } + + SendMessage(sb.ToString(), PlayerId); + } + public static void CopyCurrentSettings() + { + var sb = new StringBuilder(); + if (Options.HideGameSettings.GetBool() && !AmongUsClient.Instance.AmHost) + { + ClipboardHelper.PutClipboardString(GetString("Message.HideGameSettings")); + return; + } + sb.Append($"━━━━━━━━━━━━【{GetString("Roles")}】━━━━━━━━━━━━"); + foreach (var role in Options.CustomRoleCounts) + { + if (!role.Key.IsEnable()) continue; + string mode = role.Key.GetMode() == 1 ? GetString("RoleRateNoColor") : GetString("RoleOnNoColor"); + sb.Append($"\n【{GetRoleName(role.Key)}:{mode} ×{role.Key.GetCount()}】\n"); + ShowChildrenSettings(Options.CustomRoleSpawnChances[role.Key], ref sb); + var text = sb.ToString(); + sb.Clear().Append(text.RemoveHtmlTags()); + } + sb.Append($"━━━━━━━━━━━━【{GetString("Settings")}】━━━━━━━━━━━━"); + foreach (var opt in OptionItem.AllOptions.Where(x => x.GetBool() && x.Parent == null && x.Id >= 80000 && !x.IsHiddenOn(Options.CurrentGameMode))) + { + if (opt.Name == "KillFlashDuration") + sb.Append($"\n【{opt.GetName(true)}: {opt.GetString()}】\n"); + else + sb.Append($"\n【{opt.GetName(true)}】\n"); + ShowChildrenSettings(opt, ref sb); + var text = sb.ToString(); + sb.Clear().Append(text.RemoveHtmlTags()); + } + sb.Append($"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + ClipboardHelper.PutClipboardString(sb.ToString()); + } + public static void ShowActiveRoles(byte PlayerId = byte.MaxValue) + { + if (Options.HideGameSettings.GetBool() && PlayerId != byte.MaxValue) + { + SendMessage(GetString("Message.HideGameSettings"), PlayerId); + return; + } + + var sb = new StringBuilder(); + sb.AppendFormat("\n{0}: {1}", GetRoleName(CustomRoles.GM), Options.EnableGM.GetString().RemoveHtmlTags()); + + var impsb = new StringBuilder(); + var neutralsb = new StringBuilder(); + var crewsb = new StringBuilder(); + var addonsb = new StringBuilder(); + //int headCount = -1; + foreach (CustomRoles role in Enum.GetValues(typeof(CustomRoles))) + { + string mode = role.GetMode() == 1 ? GetString("RoleRateNoColor") : GetString("RoleOnNoColor"); + if (role.IsEnable()) + { + var roleDisplay = $"\n{GetRoleName(role)}:{mode} x{role.GetCount()}"; + if (role.IsAdditionRole()) addonsb.Append(roleDisplay); + else if (role.IsCrewmate()) crewsb.Append(roleDisplay); + else if (role.IsImpostor() || role.IsMadmate()) impsb.Append(roleDisplay); + else if (role.IsNeutral()) neutralsb.Append(roleDisplay); + } + + + //headCount++; + //if (role.IsImpostor() && headCount == 0) sb.Append("\n\n● " + GetString("TabGroup.ImpostorRoles")); + //else if (role.IsCrewmate() && headCount == 1) sb.Append("\n\n● " + GetString("TabGroup.CrewmateRoles")); + //else if (role.IsNeutral() && headCount == 2) sb.Append("\n\n● " + GetString("TabGroup.NeutralRoles")); + //else if (role.IsAdditionRole() && headCount == 3) sb.Append("\n\n● " + GetString("TabGroup.Addons")); + //else headCount--; + + //string mode = role.GetMode() == 1 ? GetString("RoleRateNoColor") : GetString("RoleOnNoColor"); + //if (role.IsEnable()) sb.AppendFormat("\n{0}:{1} x{2}", GetRoleName(role), $"{mode}", role.GetCount()); + } + // SendMessage(sb.ToString(), PlayerId); + SendMessage(sb.Append("\n.").ToString(), PlayerId, "【 ★ Roles ★ 】"); + SendMessage(impsb.Append("\n.").ToString(), PlayerId, ColorString(GetRoleColor(CustomRoles.Impostor), "【 ★ Impostor Roles ★ 】")); + SendMessage(crewsb.Append("\n.").ToString(), PlayerId, ColorString(Utils.GetRoleColor(CustomRoles.Crewmate), "【 ★ Crewmate Roles ★ 】")); + SendMessage(neutralsb.Append("\n.").ToString(), PlayerId, "【 ★ Neutral Roles ★ 】"); + SendMessage(addonsb.Append("\n.").ToString(), PlayerId, "【 ★ Add-ons ★ 】"); + //foreach (string roleList in sb.ToString().Split("\n\n●")) + // SendMessage("\n\n●" + roleList + "\n\n.", PlayerId); + } + public static void ShowChildrenSettings(OptionItem option, ref StringBuilder sb, int deep = 0, bool command = false) + { + foreach (var opt in option.Children.Select((v, i) => new { Value = v, Index = i + 1 })) + { + if (command) + { + sb.Append("\n\n"); + command = false; + } + + if (opt.Value.Name == "Maximum") continue; //Maximumの項目は飛ばす + if (opt.Value.Name == "DisableSkeldDevices" && !Options.IsActiveSkeld) continue; + if (opt.Value.Name == "DisableMiraHQDevices" && !Options.IsActiveMiraHQ) continue; + if (opt.Value.Name == "DisablePolusDevices" && !Options.IsActivePolus) continue; + if (opt.Value.Name == "DisableAirshipDevices" && !Options.IsActiveAirship) continue; + if (opt.Value.Name == "PolusReactorTimeLimit" && !Options.IsActivePolus) continue; + if (opt.Value.Name == "AirshipReactorTimeLimit" && !Options.IsActiveAirship) continue; + if (deep > 0) + { + sb.Append(string.Concat(Enumerable.Repeat("┃", Mathf.Max(deep - 1, 0)))); + sb.Append(opt.Index == option.Children.Count ? "┗ " : "┣ "); + } + sb.Append($"{opt.Value.GetName(true)}: {opt.Value.GetString()}\n"); + if (opt.Value.GetBool()) ShowChildrenSettings(opt.Value, ref sb, deep + 1); + } + } + public static void ShowLastRoles(byte PlayerId = byte.MaxValue) + { + if (AmongUsClient.Instance.IsGameStarted) + { + SendMessage(GetString("CantUse.lastroles"), PlayerId); + return; + } + var sb = new StringBuilder(); + + sb.Append(GetString("PlayerInfo")).Append(":"); + List cloneRoles = new(Main.PlayerStates.Keys); + foreach (var id in Main.winnerList) + { + if (EndGamePatch.SummaryText[id].Contains("")) continue; + sb.Append($"\n★ ").Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + cloneRoles.Remove(id); + } + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + { + List<(int, byte)> list = new(); + foreach (var id in cloneRoles) list.Add((SoloKombatManager.GetRankOfScore(id), id)); + list.Sort(); + foreach (var id in list) + sb.Append($"\n ").Append(EndGamePatch.SummaryText[id.Item2]); + } + else + { + foreach (var id in cloneRoles) + { + if (EndGamePatch.SummaryText[id].Contains("")) continue; + sb.Append($"\n ").Append(EndGamePatch.SummaryText[id].RemoveHtmlTags()); + } + } + SendMessage(sb.ToString(), PlayerId); + } + public static void ShowKillLog(byte PlayerId = byte.MaxValue) + { + if (GameStates.IsInGame) + { + SendMessage(GetString("CantUse.killlog"), PlayerId); + return; + } + if (EndGamePatch.KillLog != "") SendMessage(EndGamePatch.KillLog, PlayerId); + } + public static void ShowLastResult(byte PlayerId = byte.MaxValue) + { + if (GameStates.IsInGame) + { + SendMessage(GetString("CantUse.lastresult"), PlayerId); + return; + } + var sb = new StringBuilder(); + if (SetEverythingUpPatch.LastWinsText != "") sb.Append($"{GetString("LastResult")}: {SetEverythingUpPatch.LastWinsText}"); + if (SetEverythingUpPatch.LastWinsReason != "") sb.Append($"\n{GetString("LastEndReason")}: {SetEverythingUpPatch.LastWinsReason}"); + if (sb.Length > 0 && Options.CurrentGameMode != CustomGameMode.SoloKombat) SendMessage(sb.ToString(), PlayerId); + } + public static string GetSubRolesText(byte id, bool disableColor = false, bool intro = false, bool summary = false) + { + var SubRoles = Main.PlayerStates[id].SubRoles; + if (SubRoles.Count == 0 && intro == false) return ""; + var sb = new StringBuilder(); + foreach (var role in SubRoles) + { + if (role is CustomRoles.NotAssigned or + CustomRoles.LastImpostor) continue; + if (summary && role is CustomRoles.Madmate or CustomRoles.Charmed or CustomRoles.Recruit or CustomRoles.Admired or CustomRoles.Infected or CustomRoles.Contagious or CustomRoles.Soulless) continue; + + var RoleText = disableColor ? GetRoleName(role) : ColorString(GetRoleColor(role), GetRoleName(role)); + sb.Append($"{ColorString(Color.gray, " + ")}{RoleText}"); + } + + if (intro && !SubRoles.Contains(CustomRoles.Lovers) && !SubRoles.Contains(CustomRoles.Ntr) && CustomRolesHelper.RoleExist(CustomRoles.Ntr)) + { + var RoleText = disableColor ? GetRoleName(CustomRoles.Lovers) : ColorString(GetRoleColor(CustomRoles.Lovers), GetRoleName(CustomRoles.Lovers)); + sb.Append($"{ColorString(Color.gray, " + ")}{RoleText}"); + } + + return sb.ToString(); + } + + public static byte MsgToColor(string text, bool isHost = false) + { + text = text.ToLowerInvariant(); + text = text.Replace("色", string.Empty); + int color = -1; + try { color = int.Parse(text); } catch { color = -1; } + switch (text) + { + case "0": + case "红": + case "紅": + case "red": + case "Red": + case "крас": + case "Крас": + case "красн": + case "Красн": + case "красный": + case "Красный": + color = 0; break; + case "1": + case "蓝": + case "藍": + case "深蓝": + case "blue": + case "Blue": + case "син": + case "Син": + case "синий": + case "Синий": + color = 1; break; + case "2": + case "绿": + case "綠": + case "深绿": + case "green": + case "Green": + case "Зел": + case "зел": + case "Зелёный": + case "Зеленый": + case "зелёный": + case "зеленый": + color = 2; break; + case "3": + case "粉红": + case "pink": + case "Pink": + case "Роз": + case "роз": + case "Розовый": + case "розовый": + color = 3; break; + case "4": + case "橘": + case "orange": + case "Orange": + case "оранж": + case "Оранж": + case "оранжевый": + case "Оранжевый": + color = 4; break; + case "5": + case "黄": + case "黃": + case "yellow": + case "Yellow": + case "Жёлт": + case "Желт": + case "жёлт": + case "желт": + case "Жёлтый": + case "Желтый": + case "жёлтый": + case "желтый": + color = 5; break; + case "6": + case "黑": + case "black": + case "Black": + case "Чёрный": + case "Черный": + case "чёрный": + case "черный": + color = 6; break; + case "7": + case "白": + case "white": + case "White": + case "Белый": + case "белый": + color = 7; break; + case "8": + case "紫": + case "purple": + case "Purple": + case "Фиол": + case "фиол": + case "Фиолетовый": + case "фиолетовый": + color = 8; break; + case "9": + case "棕": + case "brown": + case "Brown": + case "Корич": + case "корич": + case "Коричневый": + case "коричевый": + color = 9; break; + case "10": + case "青": + case "cyan": + case "Cyan": + case "Голуб": + case "голуб": + case "Голубой": + case "голубой": + color = 10; break; + case "11": + case "黄绿": + case "黃綠": + case "浅绿": + case "lime": + case "Lime": + case "Лайм": + case "лайм": + case "Лаймовый": + case "лаймовый": + color = 11; break; + case "12": + case "红褐": + case "紅褐": + case "深红": + case "maroon": + case "Maroon": + case "Борд": + case "борд": + case "Бордовый": + case "бордовый": + color = 12; break; + case "13": + case "玫红": + case "玫紅": + case "浅粉": + case "rose": + case "Rose": + case "Светло роз": + case "светло роз": + case "Светло розовый": + case "светло розовый": + case "Сирень": + case "сирень": + case "Сиреневый": + case "сиреневый": + color = 13; break; + case "14": + case "焦黄": + case "焦黃": + case "淡黄": + case "banana": + case "Banana": + case "Банан": + case "банан": + case "Банановый": + case "банановый": + color = 14; break; + case "15": + case "灰": + case "gray": + case "Gray": + case "Сер": + case "сер": + case "Серый": + case "серый": + color = 15; break; + case "16": + case "茶": + case "tan": + case "Tan": + case "Загар": + case "загар": + case "Загаровый": + case "загаровый": + color = 16; break; + case "17": + case "珊瑚": + case "coral": + case "Coral": + case "Корал": + case "корал": + case "Коралл": + case "коралл": + case "Коралловый": + case "коралловый": + color = 17; break; + + case "18": case "隐藏": case "?": color = 18; break; + } + return !isHost && color == 18 ? byte.MaxValue : color is < 0 or > 18 ? byte.MaxValue : Convert.ToByte(color); + } + + public static void ShowHelpToClient(byte ID) + { + SendMessage( + GetString("CommandList") + + $"\n ○ /n {GetString("Command.now")}" + + $"\n ○ /r {GetString("Command.roles")}" + + $"\n ○ /m {GetString("Command.myrole")}" + + $"\n ○ /xf {GetString("Command.solvecover")}" + + $"\n ○ /l {GetString("Command.lastresult")}" + + $"\n ○ /win {GetString("Command.winner")}" + + "\n\n" + GetString("CommandOtherList") + + $"\n ○ /color {GetString("Command.color")}" + + $"\n ○ /qt {GetString("Command.quit")}" + , ID); + } + public static void ShowHelp(byte ID) + { + SendMessage( + GetString("CommandList") + + $"\n ○ /n {GetString("Command.now")}" + + $"\n ○ /r {GetString("Command.roles")}" + + $"\n ○ /m {GetString("Command.myrole")}" + + $"\n ○ /l {GetString("Command.lastresult")}" + + $"\n ○ /win {GetString("Command.winner")}" + + "\n\n" + GetString("CommandOtherList") + + $"\n ○ /color {GetString("Command.color")}" + + $"\n ○ /rn {GetString("Command.rename")}" + + $"\n ○ /qt {GetString("Command.quit")}" + + "\n\n" + GetString("CommandHostList") + + $"\n ○ /s {GetString("Command.say")}" + + $"\n ○ /rn {GetString("Command.rename")}" + + $"\n ○ /xf {GetString("Command.solvecover")}" + + $"\n ○ /mw {GetString("Command.mw")}" + + $"\n ○ /kill {GetString("Command.kill")}" + + $"\n ○ /exe {GetString("Command.exe")}" + + $"\n ○ /level {GetString("Command.level")}" + + $"\n ○ /id {GetString("Command.idlist")}" + + $"\n ○ /qq {GetString("Command.qq")}" + + $"\n ○ /dump {GetString("Command.dump")}" + , ID); + } + public static void CheckTerroristWin(GameData.PlayerInfo Terrorist) + { + if (!AmongUsClient.Instance.AmHost) return; + var taskState = GetPlayerById(Terrorist.PlayerId).GetPlayerTaskState(); + if (taskState.IsTaskFinished && (!Main.PlayerStates[Terrorist.PlayerId].IsSuicide() || Options.CanTerroristSuicideWin.GetBool())) //タスクが完了で(自殺じゃない OR 自殺勝ちが許可)されていれば + { + foreach (var pc in Main.AllPlayerControls) + { + if (pc.Is(CustomRoles.Terrorist)) + { + if (Main.PlayerStates[pc.PlayerId].deathReason == PlayerState.DeathReason.Vote) + { + //追放された場合は生存扱い + Main.PlayerStates[pc.PlayerId].deathReason = PlayerState.DeathReason.etc; + //生存扱いのためSetDeadは必要なし + } + else + { + //キルされた場合は自爆扱い + Main.PlayerStates[pc.PlayerId].deathReason = PlayerState.DeathReason.Suicide; + } + } + else if (!pc.Data.IsDead) + { + //生存者は爆死 + pc.SetRealKiller(Terrorist.Object); + pc.RpcMurderPlayerV3(pc); + Main.PlayerStates[pc.PlayerId].deathReason = PlayerState.DeathReason.Bombed; + Main.PlayerStates[pc.PlayerId].SetDead(); + } + } + CustomWinnerHolder.ResetAndSetWinner(CustomWinner.Terrorist); + CustomWinnerHolder.WinnerIds.Add(Terrorist.PlayerId); + } + } + public static void SendMessage(string text, byte sendTo = byte.MaxValue, string title = "") + { + if (!AmongUsClient.Instance.AmHost) return; + if (title == "") title = "" + GetString("DefaultSystemMessageTitle") + ""; + Main.MessagesToSend.Add((text.RemoveHtmlTagsTemplate(), sendTo, title)); + } + public static void ApplySuffix(PlayerControl player) + { + if (!AmongUsClient.Instance.AmHost || player == null) return; + if (!(player.AmOwner || (player.FriendCode.GetDevUser().HasTag()))) return; + string name = Main.AllPlayerNames.TryGetValue(player.PlayerId, out var n) ? n : ""; + if (Main.nickName != "" && player.AmOwner) name = Main.nickName; + if (name == "") return; + if (AmongUsClient.Instance.IsGameStarted) + { + if (Options.FormatNameMode.GetInt() == 1 && Main.nickName == "") name = Palette.GetColorName(player.Data.DefaultOutfit.ColorId); + } + else + { + if (!GameStates.IsLobby) return; + if (player.AmOwner) + { + if (GameStates.IsOnlineGame || GameStates.IsLocalGame) + name = $"{GetString("HostText")}{GetString("Icon")}{name}"; + + //name = $"{GetString("HostText")}♥" + name; + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + name = $"{GetString("ModeSoloKombat")}\r\n" + name; + } + if (!name.Contains('\r') && player.FriendCode.GetDevUser().HasTag()) + name = player.FriendCode.GetDevUser().GetTag() + name; + else + name = Options.GetSuffixMode() switch + { + SuffixModes.TOHE => name += $"\r\nTOHE-R v{Main.PluginDisplayVersion}", + SuffixModes.Streaming => name += $"\r\n{GetString("SuffixMode.Streaming")}", + SuffixModes.Recording => name += $"\r\n{GetString("SuffixMode.Recording")}", + SuffixModes.RoomHost => name += $"\r\n{GetString("SuffixMode.RoomHost")}", + SuffixModes.OriginalName => name += $"\r\n{DataManager.player.Customization.Name}", + SuffixModes.DoNotKillMe => name += $"\r\n{GetString("SuffixModeText.DoNotKillMe")}", + SuffixModes.NoAndroidPlz => name += $"\r\n{GetString("SuffixModeText.NoAndroidPlz")}", + SuffixModes.AutoHost => name += $"\r\n{GetString("SuffixModeText.AutoHost")}", + _ => name + }; + } + if (name != player.name && player.CurrentOutfitType == PlayerOutfitType.Default) + player.RpcSetName(name); + } + public static PlayerControl GetPlayerById(int PlayerId) + { + return Main.AllPlayerControls.Where(pc => pc.PlayerId == PlayerId).FirstOrDefault(); + } + public static GameData.PlayerInfo GetPlayerInfoById(int PlayerId) => + GameData.Instance.AllPlayers.ToArray().Where(info => info.PlayerId == PlayerId).FirstOrDefault(); + private static StringBuilder SelfSuffix = new(); + private static StringBuilder SelfMark = new(20); + private static StringBuilder TargetSuffix = new(); + private static StringBuilder TargetMark = new(20); + public static void NotifyRoles(bool isForMeeting = false, PlayerControl SpecifySeer = null, bool NoCache = false, bool ForceLoop = false, bool CamouflageIsForMeeting = false, bool GuesserIsForMeeting = false) + { + if (!AmongUsClient.Instance.AmHost) return; + if (Main.AllPlayerControls == null) return; + + //ミーティング中の呼び出しは不正 + if (GameStates.IsMeeting) return; + + var caller = new System.Diagnostics.StackFrame(1, false); + var callerMethod = caller.GetMethod(); + string callerMethodName = callerMethod.Name; + string callerClassName = callerMethod.DeclaringType.FullName; + var logger = Logger.Handler("NotifyRoles"); + logger.Info("NotifyRolesが" + callerClassName + "." + callerMethodName + "から呼び出されました"); + HudManagerPatch.NowCallNotifyRolesCount++; + HudManagerPatch.LastSetNameDesyncCount = 0; + + var seerList = PlayerControl.AllPlayerControls; + if (SpecifySeer != null) + { + seerList = new(); + seerList.Add(SpecifySeer); + } + //seer:ここで行われた変更を見ることができるプレイヤー + //target:seerが見ることができる変更の対象となるプレイヤー + foreach (var seer in seerList) + { + //seerが落ちているときに何もしない + if (seer == null || seer.Data.Disconnected) continue; + + if (seer.IsModClient()) continue; + string fontSize = "1.5"; + if (isForMeeting && (seer.GetClient().PlatformData.Platform == Platforms.Playstation || seer.GetClient().PlatformData.Platform == Platforms.Switch)) fontSize = "70%"; + logger.Info("NotifyRoles-Loop1-" + seer.GetNameWithRole() + ":START"); + + //タスクなど進行状況を含むテキスト + string SelfTaskText = GetProgressText(seer); + + //名前の後ろに付けるマーカー + SelfMark.Clear(); + + //インポスター/キル可能なニュートラルに対するSnitch警告 + SelfMark.Append(Snitch.GetWarningArrow(seer)); + + //ハートマークを付ける(自分に) + if (seer.Is(CustomRoles.Lovers) || CustomRolesHelper.RoleExist(CustomRoles.Ntr)) SelfMark.Append(ColorString(GetRoleColor(CustomRoles.Lovers), "♥")); + + //呪われている場合 + SelfMark.Append(Witch.GetSpelledMark(seer.PlayerId, isForMeeting)); + SelfMark.Append(HexMaster.GetHexedMark(seer.PlayerId, isForMeeting)); + if (Baker.IsPoisoned(seer) && isForMeeting && seer.IsAlive()) + SelfMark.Append(ColorString(GetRoleColor(CustomRoles.Famine), "θ")); + + + //如果是大明星 + if (seer.Is(CustomRoles.SuperStar) && Options.EveryOneKnowSuperStar.GetBool()) + SelfMark.Append(ColorString(GetRoleColor(CustomRoles.SuperStar), "★")); + + //球状闪电提示 + if (BallLightning.IsGhost(seer)) + SelfMark.Append(ColorString(GetRoleColor(CustomRoles.BallLightning), "■")); + + //医生护盾提示 + if ((Medic.InProtect(seer.PlayerId) || Medic.TempMarkProtected == seer.PlayerId) && !seer.Is(CustomRoles.Medic) && (Medic.WhoCanSeeProtect.GetInt() == 0 || Medic.WhoCanSeeProtect.GetInt() == 2)) + SelfMark.Append(ColorString(GetRoleColor(CustomRoles.Medic), " ●")); + + //玩家自身血量提示 + SelfMark.Append(Gamer.TargetMark(seer, seer)); + + //銃声が聞こえるかチェック + SelfMark.Append(Sniper.GetShotNotify(seer.PlayerId)); + //Markとは違い、改行してから追記されます。 + SelfSuffix.Clear(); + + if (seer.Is(CustomRoles.BountyHunter) && !isForMeeting) + { + SelfSuffix.Append(BountyHunter.GetTargetText(seer, false)); + SelfSuffix.Append(BountyHunter.GetTargetArrow(seer)); + } + if (seer.Is(CustomRoles.Mortician) && !isForMeeting) + { + SelfSuffix.Append(Mortician.GetTargetArrow(seer)); + } + if (seer.Is(CustomRoles.Tracefinder) && !isForMeeting) + { + SelfSuffix.Append(Tracefinder.GetTargetArrow(seer)); + } + if (seer.Is(CustomRoles.Vulture) && Vulture.ArrowsPointingToDeadBody.GetBool() && !isForMeeting) + { + SelfSuffix.Append(Vulture.GetTargetArrow(seer)); + } + if (seer.Is(CustomRoles.FireWorks) && !isForMeeting) + { + string stateText = FireWorks.GetStateText(seer); + SelfSuffix.Append(stateText); + } + if (seer.Is(CustomRoles.Witch)) + { + SelfSuffix.Append(Witch.GetSpellModeText(seer, false, isForMeeting)); + } + if (seer.Is(CustomRoles.HexMaster)) + { + SelfSuffix.Append(HexMaster.GetHexModeText(seer, false, isForMeeting)); + } + if (seer.Is(CustomRoles.AntiAdminer) && !isForMeeting) + { + if (AntiAdminer.IsAdminWatch) SelfSuffix.Append("★").Append(GetString("AntiAdminerAD")); + if (AntiAdminer.IsVitalWatch) SelfSuffix.Append("★").Append(GetString("AntiAdminerVI")); + if (AntiAdminer.IsDoorLogWatch) SelfSuffix.Append("★").Append(GetString("AntiAdminerDL")); + if (AntiAdminer.IsCameraWatch) SelfSuffix.Append("★").Append(GetString("AntiAdminerCA")); + } + if (seer.Is(CustomRoles.Bloodhound) && !isForMeeting) + { + SelfSuffix.Append(Bloodhound.GetTargetArrow(seer)); + } + if (seer.Is(CustomRoles.Tracker) && !isForMeeting) + { + SelfSuffix.Append(Tracker.GetTrackerArrow(seer)); + } + if (seer.Is(CustomRoles.Spiritualist) && !isForMeeting) + { + SelfSuffix.Append(Spiritualist.GetSpiritualistArrow(seer)); + } + + //タスクを終えたSnitchがインポスター/キル可能なニュートラルの方角を確認できる + if (seer.Is(CustomRoles.Snitch) && !isForMeeting) + { + SelfSuffix.Append(Snitch.GetSnitchArrow(seer)); + } + if (seer.Is(CustomRoles.EvilTracker) && !isForMeeting) + { + SelfSuffix.Append(EvilTracker.GetTargetArrow(seer, seer)); + } + + SelfSuffix.Append(Deathpact.GetDeathpactPlayerArrow(seer)); + + + + //KB自身名字后缀 + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + SelfSuffix.Append(SoloKombatManager.GetDisplayHealth(seer)); + + //RealNameを取得 なければ現在の名前をRealNamesに書き込む + string SeerRealName = seer.GetRealName(isForMeeting); + + if (seer.GetCustomRole().IsCrewmate() && !seer.Is(CustomRoles.Madmate)) + { + if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool()) + SeerRealName = $"" + GetString("YouAreCrewmate") + $"\n" + seer.GetRoleInfo(); + } + if (seer.GetCustomRole().IsImpostor()) + { + if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool()) + SeerRealName = $"" + GetString("YouAreImpostor") + $"\n" + seer.GetRoleInfo() + $""; + } + if (seer.GetCustomRole().IsNeutral() && !seer.GetCustomRole().IsMadmate()) + { + if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool()) + SeerRealName = $"" + GetString("YouAreNeutral") + $"\n" + seer.GetRoleInfo() + $""; + } + if (seer.GetCustomRole().IsMadmate()) + { + if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool()) + SeerRealName = $"" + GetString("YouAreMadmate") + $"\n" + seer.GetRoleInfo() + $""; + } + if (seer.Is(CustomRoles.Madmate)) + { + if (!isForMeeting && MeetingStates.FirstMeeting && Options.ChangeNameToRoleInfo.GetBool()) + SeerRealName = $"" + GetString("YouAreMadmate") + $"\n" + seer.GetRoleInfo() + $""; + } + //seerの役職名とSelfTaskTextとseerのプレイヤー名とSelfMarkを合成 + string SelfRoleName = $"{seer.GetDisplayRoleName()}{SelfTaskText}"; + string SelfDeathReason = seer.KnowDeathReason(seer) ? $"({ColorString(GetRoleColor(CustomRoles.Doctor), GetVitalText(seer.PlayerId))})" : ""; + string SelfName = $"{ColorString(seer.GetRoleColor(), SeerRealName)}{SelfDeathReason}{SelfMark}"; + if (seer.Is(CustomRoles.PlagueBearer) && PlagueBearer.IsPlaguedAll(seer)) + { + seer.RpcSetCustomRole(CustomRoles.Pestilence); + seer.Notify(GetString("PlagueBearerToPestilence")); + seer.RpcGuardAndKill(seer); + if (!PlagueBearer.PestilenceList.Contains(seer.PlayerId)) + PlagueBearer.PestilenceList.Add(seer.PlayerId); + PlagueBearer.SetKillCooldownPestilence(seer.PlayerId); + PlagueBearer.playerIdList.Remove(seer.PlayerId); + } + + if (seer.Is(CustomRoles.Arsonist) && seer.IsDouseDone()) + SelfName = $"{ColorString(seer.GetRoleColor(), GetString("EnterVentToWin"))}"; + if (seer.Is(CustomRoles.Revolutionist) && seer.IsDrawDone()) + SelfName = $">{ColorString(seer.GetRoleColor(), string.Format(GetString("EnterVentWinCountDown"), Main.RevolutionistCountdown.TryGetValue(seer.PlayerId, out var x) ? x : 10))}"; + + if (Pelican.IsEaten(seer.PlayerId)) + SelfName = $"{ColorString(GetRoleColor(CustomRoles.Pelican), GetString("EatenByPelican"))}"; + if (Deathpact.IsInActiveDeathpact(seer)) + SelfName = Deathpact.GetDeathpactString(seer); + if (NameNotifyManager.GetNameNotify(seer, out var name)) + SelfName = name; + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + { + SoloKombatManager.GetNameNotify(seer, ref SelfName); + SelfName = $"{SelfTaskText}\r\n{SelfName}"; + } + else SelfName = SelfRoleName + "\r\n" + SelfName; + SelfName += SelfSuffix.ToString() == "" ? "" : "\r\n " + SelfSuffix.ToString(); + bool playerDevoured = Devourer.HideNameOfConsumedPlayer.GetBool() && Devourer.PlayerSkinsCosumed.Any(a => a.Value.Contains(seer.PlayerId)); + if (((IsActive(SystemTypes.Comms) && Options.CommsCamouflage.GetBool()) || Camouflager.IsActive || playerDevoured) && !CamouflageIsForMeeting) + SelfName = SelfRoleName; + if (!isForMeeting) SelfName += "\r\n"; + + //適用 + seer.RpcSetNamePrivate(SelfName, true, force: NoCache); + + //seerが死んでいる場合など、必要なときのみ第二ループを実行する + foreach (var target in Main.AllPlayerControls) + { + //targetがseer自身の場合は何もしない + if (target.PlayerId == seer.PlayerId) continue; + logger.Info("NotifyRoles-Loop2-" + target.GetNameWithRole() + ":START"); + + //名前の後ろに付けるマーカー + TargetMark.Clear(); + + //呪われている人 + TargetMark.Append(Witch.GetSpelledMark(target.PlayerId, isForMeeting)); + TargetMark.Append(HexMaster.GetHexedMark(target.PlayerId, isForMeeting)); + // TargetMark.Append(Baker.GetPoisonMark(target, isForMeeting)); + + + //如果是大明星 + if (target.Is(CustomRoles.SuperStar) && Options.EveryOneKnowSuperStar.GetBool()) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.SuperStar), "★")); + + + // Necroview + /* if (seer.Is(CustomRoles.Necroview) && isForMeeting) + { + if (target.Is(CustomRoleTypes.Crewmate) && !(target.Is(CustomRoles.Madmate) || target.Is(CustomRoles.Egoist) || target.Is(CustomRoles.Charmed) || target.Is(CustomRoles.Recruit) || target.Is(CustomRoles.Infected) || target.Is(CustomRoles.Contagious) || target.Is(CustomRoles.Rogue) || target.Is(CustomRoles.Rascal) || target.Is(CustomRoles.Soulless)) && target.Data.IsDead || target.Is(CustomRoles.Admired) && target.Data.IsDead) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Bait), "★")); + + if ((target.Is(CustomRoleTypes.Impostor) || target.Is(CustomRoles.Madmate) || target.Is(CustomRoles.Rascal) || target.Is(CustomRoles.Parasite) || target.Is(CustomRoles.Refugee) || target.Is(CustomRoles.Crewpostor) || target.Is(CustomRoles.Convict)) && target.Data.IsDead || !target.Is(CustomRoles.Admired) && target.Data.IsDead) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Impostor), "★")); + + if ((target.Is(CustomRoleTypes.Neutral) || target.Is(CustomRoles.Rogue) || target.Is(CustomRoles.Contagious) || target.Is(CustomRoles.Charmed) || target.Is(CustomRoles.Recruit) || target.Is(CustomRoles.Infected) || target.Is(CustomRoles.Egoist) || target.Is(CustomRoles.Soulless)) && target.Data.IsDead || !target.Is(CustomRoles.Admired) && target.Data.IsDead || (target.Is(CustomRoles.Parasite) || target.Is(CustomRoles.Refugee) || target.Is(CustomRoles.Crewpostor) || target.Is(CustomRoles.Convict)) && target.Data.IsDead) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Executioner), "★")); + } + // Visionary + if (seer.Is(CustomRoles.Visionary) && isForMeeting) + { + if (target.Is(CustomRoleTypes.Crewmate) && !(target.Is(CustomRoles.Madmate) || target.Is(CustomRoles.Egoist) || target.Is(CustomRoles.Charmed) || target.Is(CustomRoles.Recruit) || target.Is(CustomRoles.Infected) || target.Is(CustomRoles.Contagious) || target.Is(CustomRoles.Rogue) || target.Is(CustomRoles.Rascal) || target.Is(CustomRoles.Soulless)) && !target.Data.IsDead || target.Is(CustomRoles.Admired) && !target.Data.IsDead) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Bait), "★")); + + if ((target.Is(CustomRoleTypes.Impostor) || target.Is(CustomRoles.Madmate) || target.Is(CustomRoles.Rascal) || target.Is(CustomRoles.Parasite) || target.Is(CustomRoles.Refugee) || target.Is(CustomRoles.Crewpostor) || target.Is(CustomRoles.Convict)) && !target.Data.IsDead || !target.Is(CustomRoles.Admired) && !target.Data.IsDead) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Impostor), "★")); + + if ((target.Is(CustomRoleTypes.Neutral) || target.Is(CustomRoles.Rogue) || target.Is(CustomRoles.Contagious) || target.Is(CustomRoles.Charmed) || target.Is(CustomRoles.Recruit) || target.Is(CustomRoles.Infected) || target.Is(CustomRoles.Egoist) || target.Is(CustomRoles.Soulless)) && !target.Data.IsDead || !target.Is(CustomRoles.Admired) && !target.Data.IsDead || (target.Is(CustomRoles.Parasite) || target.Is(CustomRoles.Refugee) || target.Is(CustomRoles.Crewpostor) || target.Is(CustomRoles.Convict)) && !target.Data.IsDead) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Executioner), "★")); + } */ + + + //球状闪电提示 + if (BallLightning.IsGhost(target)) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.BallLightning), "■")); + + //タスク完了直前のSnitchにマークを表示 + TargetMark.Append(Snitch.GetWarningMark(seer, target)); + + //ハートマークを付ける(相手に) + if (seer.Is(CustomRoles.Lovers) && target.Is(CustomRoles.Lovers)) + { + TargetMark.Append($"♥"); + } + //霊界からラバーズ視認 + else if (seer.Data.IsDead && !seer.Is(CustomRoles.Lovers) && target.Is(CustomRoles.Lovers)) + { + TargetMark.Append($"♥"); + } + else if (target.Is(CustomRoles.Ntr) || seer.Is(CustomRoles.Ntr)) + { + TargetMark.Append($"♥"); + } + + if (seer.Is(CustomRoles.PlagueBearer)) + { + if (PlagueBearer.isPlagued(seer.PlayerId, target.PlayerId)) + { + TargetMark.Append($"●"); + PlagueBearer.SendRPC(seer, target); + } + } + + if (seer.Is(CustomRoles.Arsonist))//seerがアーソニストの時 + { + if (seer.IsDousedPlayer(target)) //seerがtargetに既にオイルを塗っている(完了) + { + TargetMark.Append($"▲"); + } + if ( + Main.ArsonistTimer.TryGetValue(seer.PlayerId, out var ar_kvp) && //seerがオイルを塗っている途中(現在進行) + ar_kvp.Item1 == target //オイルを塗っている対象がtarget + ) + { + TargetMark.Append($"△"); + } + } + if (seer.Is(CustomRoles.Revolutionist))//seer是革命家时 + { + if (seer.IsDrawPlayer(target)) //seer已完成拉拢船员 + { + TargetMark.Append($"●"); + } + if (Main.RevolutionistTimer.TryGetValue(seer.PlayerId, out var ar_kvp) && ar_kvp.Item1 == target)//seer正在拉拢船员 + { + TargetMark.Append($"○"); + } + } + if (seer.Is(CustomRoles.Farseer))//seerがアーソニストの時 + { + if ( + Main.FarseerTimer.TryGetValue(seer.PlayerId, out var ar_kvp) && //seerがオイルを塗っている途中(現在進行) + ar_kvp.Item1 == target //オイルを塗っている対象がtarget + ) + { + TargetMark.Append($"○"); + } + } + if (seer.Is(CustomRoles.Puppeteer) && + Main.PuppeteerList.ContainsValue(seer.PlayerId) && + Main.PuppeteerList.ContainsKey(target.PlayerId)) + TargetMark.Append($"◆"); + if (seer.Is(CustomRoles.NWitch) && + Main.TaglockedList.ContainsValue(seer.PlayerId) && + Main.TaglockedList.ContainsKey(target.PlayerId)) + TargetMark.Append($"◆"); + + //他人の役職とタスクは幽霊が他人の役職を見れるようになっていてかつ、seerが死んでいる場合のみ表示されます。それ以外の場合は空になります。 + string TargetRoleText = + (seer.Data.IsDead && Options.GhostCanSeeOtherRoles.GetBool()) || + (seer.Is(CustomRoles.Mimic) && target.Data.IsDead && Options.MimicCanSeeDeadRoles.GetBool()) || + (target.Is(CustomRoles.Gravestone) && target.Data.IsDead) || + (seer.Is(CustomRoles.Lovers) && target.Is(CustomRoles.Lovers) && Options.LoverKnowRoles.GetBool()) || + (target.Is(CustomRoles.Ntr) && Options.LoverKnowRoles.GetBool()) || + (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoleTypes.Impostor) && Options.ImpKnowAlliesRole.GetBool()) || + (seer.Is(CustomRoles.Madmate) && target.Is(CustomRoleTypes.Impostor) && Options.MadmateKnowWhosImp.GetBool()) || + (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoles.Madmate) && Options.ImpKnowWhosMadmate.GetBool()) || + (seer.Is(CustomRoles.Crewpostor) && target.Is(CustomRoleTypes.Impostor) && Options.CrewpostorKnowsAllies.GetBool()) || + (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoles.Crewpostor) && Options.AlliesKnowCrewpostor.GetBool()) || + (seer.Is(CustomRoles.Madmate) && target.Is(CustomRoles.Madmate) && Options.MadmateKnowWhosMadmate.GetBool()) || + (seer.Is(CustomRoles.Rogue) && target.Is(CustomRoles.Rogue) && Options.RogueKnowEachOther.GetBool() && Options.RogueKnowEachOtherRoles.GetBool()) || + (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick)) || + (seer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick)) || + (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Jackal))|| + (seer.Is(CustomRoles.Recruit) && target.Is(CustomRoles.Jackal))|| + (target.Is(CustomRoles.Workaholic) && Options.WorkaholicVisibleToEveryone.GetBool()) || + (target.Is(CustomRoles.Doctor) && !target.GetCustomRole().IsEvilAddons() && Options.DoctorVisibleToEveryone.GetBool()) || + (target.Is(CustomRoles.Mayor) && Options.MayorRevealWhenDoneTasks.GetBool() && target.GetPlayerTaskState().IsTaskFinished) || + (seer.Is(CustomRoleTypes.Crewmate) && target.Is(CustomRoles.Marshall) && target.GetPlayerTaskState().IsTaskFinished) || + (Totocalcio.KnowRole(seer, target)) || + (Lawyer.KnowRole(seer, target)) || + (EvilDiviner.IsShowTargetRole(seer, target)) || + (Ritualist.IsShowTargetRole(seer, target)) || + (Executioner.KnowRole(seer, target)) || + (Succubus.KnowRole(seer, target)) || + (CursedSoul.KnowRole(seer, target)) || + (Admirer.KnowRole(seer, target)) || + (Amnesiac.KnowRole(seer, target)) || + (Infectious.KnowRole(seer, target)) || + (Virus.KnowRole(seer, target)) || + (seer.IsRevealedPlayer(target) && !target.Is(CustomRoles.Trickster)) || + (seer.Is(CustomRoles.God)) || + (target.Is(CustomRoles.GM)) + ? $"{target.GetDisplayRoleName(seer.PlayerId != target.PlayerId && !seer.Data.IsDead)}{GetProgressText(target)}\r\n" : ""; + + if (!seer.Data.IsDead && seer.IsRevealedPlayer(target) && target.Is(CustomRoles.Trickster)) + { + TargetRoleText = Farseer.RandomRole[seer.PlayerId]; + TargetRoleText += Farseer.GetTaskState(); + } + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + TargetRoleText = $"{GetProgressText(target)}\r\n"; + + if (seer.Is(CustomRoles.EvilTracker)) + { + TargetMark.Append(EvilTracker.GetTargetMark(seer, target)); + if (isForMeeting && EvilTracker.IsTrackTarget(seer, target) && EvilTracker.CanSeeLastRoomInMeeting) + TargetRoleText = $"{EvilTracker.GetArrowAndLastRoom(seer, target)}\r\n"; + } + + if (seer.Is(CustomRoles.Tracker)) + { + TargetMark.Append(Tracker.GetTargetMark(seer, target)); + if (isForMeeting && Tracker.IsTrackTarget(seer, target) && Tracker.CanSeeLastRoomInMeeting) + TargetRoleText = $"{Tracker.GetArrowAndLastRoom(seer, target)}\r\n"; + } + + //RealNameを取得 なければ現在の名前をRealNamesに書き込む + string TargetPlayerName = target.GetRealName(isForMeeting); + + if (seer.Is(CustomRoles.Psychic) && seer.IsAlive() && target.IsRedForPsy(seer) && isForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(CustomRoles.Impostor), TargetPlayerName); + } + if (seer.Is(CustomRoles.Mafia) && !seer.IsAlive() && target.IsAlive()) + { + TargetPlayerName = ColorString(GetRoleColor(CustomRoles.Mafia), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + if (seer.Is(CustomRoles.Retributionist) && !seer.IsAlive() && target.IsAlive()) + { + TargetPlayerName = ColorString(GetRoleColor(CustomRoles.Retributionist), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + if (seer.Is(CustomRoles.Judge)) + { + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(CustomRoles.Judge), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + if (seer.Is(CustomRoles.ParityCop)) + { + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(CustomRoles.ParityCop), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + if (seer.Is(CustomRoles.Councillor)) + { + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(CustomRoles.Councillor), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + + // Guesser Mode ID + if (Options.GuesserMode.GetBool()) + { + //Crewmates + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting && !seer.Is(CustomRoles.Judge) && !seer.Is(CustomRoles.ParityCop) && Options.CrewmatesCanGuess.GetBool() && seer.GetCustomRole().IsCrewmate()) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + else if (seer.Is(CustomRoles.NiceGuesser) && !Options.CrewmatesCanGuess.GetBool()) + { + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + + //Impostors + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting && !seer.Is(CustomRoles.Councillor) && !seer.Is(CustomRoles.Mafia) && Options.ImpostorsCanGuess.GetBool() && seer.GetCustomRole().IsImpostor()) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + else if (seer.Is(CustomRoles.EvilGuesser) && !Options.ImpostorsCanGuess.GetBool()) + { + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + + // Neutrals + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting && Options.NeutralKillersCanGuess.GetBool() && seer.GetCustomRole().IsNK()) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting && Options.PassiveNeutralsCanGuess.GetBool() && seer.GetCustomRole().IsNonNK() && !seer.Is(CustomRoles.Doomsayer)) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + else // Off Guesser Mode ID + { + if (seer.Is(CustomRoles.NiceGuesser) || seer.Is(CustomRoles.EvilGuesser) || seer.Is(CustomRoles.Doomsayer) || seer.Is(CustomRoles.Guesser)) + { + if (seer.IsAlive() && target.IsAlive() && GuesserIsForMeeting) + { + TargetPlayerName = ColorString(GetRoleColor(seer.GetCustomRole()), target.PlayerId.ToString()) + " " + TargetPlayerName; + } + } + } + //ターゲットのプレイヤー名の色を書き換えます。 + TargetPlayerName = TargetPlayerName.ApplyNameColorData(seer, target, isForMeeting); + + if (seer.Is(CustomRoleTypes.Impostor) && target.Is(CustomRoles.Snitch) && target.Is(CustomRoles.Madmate) && target.GetPlayerTaskState().IsTaskFinished) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Impostor), "★")); + if (seer.Is(CustomRoleTypes.Crewmate) && target.Is(CustomRoles.Marshall) && target.GetPlayerTaskState().IsTaskFinished) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Marshall), "★")); + /*if (seer.Is(CustomRoles.Jackal) && target.Is(CustomRoles.Sidekick)) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Jackal), " ♥")); + // if (seer.Is(CustomRoles.Monarch) && target.Is(CustomRoles.Knighted)) + // TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Knighted), " 亗")); + if (seer.Is(CustomRoles.Sidekick) && target.Is(CustomRoles.Sidekick) && Options.SidekickKnowOtherSidekick.GetBool()) + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Jackal), " ♥")); */ + + TargetMark.Append(Executioner.TargetMark(seer, target)); + + // TargetMark.Append(Lawyer.TargetMark(seer, target)); + + TargetMark.Append(Gamer.TargetMark(seer, target)); + + if (seer.Is(CustomRoles.Medic) && (Medic.InProtect(target.PlayerId) || Medic.TempMarkProtected == target.PlayerId) && (Medic.WhoCanSeeProtect.GetInt() == 0 || Medic.WhoCanSeeProtect.GetInt() == 1)) + { + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Medic), " ●")); + } + else if (seer.Data.IsDead && Medic.InProtect(target.PlayerId) && !seer.Is(CustomRoles.Medic)) + { + TargetMark.Append(ColorString(GetRoleColor(CustomRoles.Medic), " ●")); + } + + TargetMark.Append(Totocalcio.TargetMark(seer, target)); + TargetMark.Append(Lawyer.LawyerMark(seer, target)); + TargetMark.Append(Deathpact.GetDeathpactMark(seer, target)); + + //KB目标玩家名字后缀 + TargetSuffix.Clear(); + + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + TargetSuffix.Append(SoloKombatManager.GetDisplayHealth(target)); + + string TargetDeathReason = ""; + if (seer.KnowDeathReason(target)) + TargetDeathReason = $"({ColorString(GetRoleColor(CustomRoles.Doctor), GetVitalText(target.PlayerId))})"; + + if (((IsActive(SystemTypes.Comms) && Options.CommsCamouflage.GetBool()) || Camouflager.IsActive) && !CamouflageIsForMeeting) + TargetPlayerName = $"{TargetPlayerName}"; + + bool targetDevoured = Devourer.HideNameOfConsumedPlayer.GetBool() && Devourer.PlayerSkinsCosumed.Any(a => a.Value.Contains(target.PlayerId)); + if (targetDevoured && !CamouflageIsForMeeting) + TargetPlayerName = GetString("DevouredName"); + + //全てのテキストを合成します。 + string TargetName = $"{TargetRoleText}{TargetPlayerName}{TargetDeathReason}{TargetMark}"; + TargetName += (TargetSuffix.ToString() == "" ? "" : ("\r\n" + TargetSuffix.ToString())); + + //適用 + target.RpcSetNamePrivate(TargetName, true, seer, force: NoCache); + + logger.Info("NotifyRoles-Loop2-" + target.GetNameWithRole() + ":END"); + } + logger.Info("NotifyRoles-Loop1-" + seer.GetNameWithRole() + ":END"); + } + } + public static void MarkEveryoneDirtySettings() + { + PlayerGameOptionsSender.SetDirtyToAll(); + } + public static void SyncAllSettings() + { + PlayerGameOptionsSender.SetDirtyToAll(); + GameOptionsSender.SendAllGameOptions(); + } + public static void AfterMeetingTasks() + { + if (Options.DiseasedCDReset.GetBool()) + { + foreach (var pid in Main.KilledDiseased.Keys) + { + Main.KilledDiseased[pid] = 0; + Utils.GetPlayerById(pid).ResetKillCooldown(); + } + Main.KilledDiseased.Clear(); + } + //Main.KilledDiseased.Clear(); + if (Options.AntidoteCDReset.GetBool()) + { + foreach (var pid in Main.KilledAntidote.Keys) + { + Main.KilledAntidote[pid] = 0; + Utils.GetPlayerById(pid).ResetKillCooldown(); + } + Main.KilledAntidote.Clear(); + } + Swooper.AfterMeetingTasks(); + Wraith.AfterMeetingTasks(); + Chameleon.AfterMeetingTasks(); + Eraser.AfterMeetingTasks(); + BountyHunter.AfterMeetingTasks(); + EvilTracker.AfterMeetingTasks(); + SerialKiller.AfterMeetingTasks(); + Spiritualist.AfterMeetingTasks(); + Vulture.AfterMeetingTasks(); + Baker.AfterMeetingTasks(); + CopyCat.AfterMeetingTasks(); + Pirate.AfterMeetingTask(); + + + if (Options.AirshipVariableElectrical.GetBool()) + AirshipElectricalDoors.Initialize(); + /* if (Options.DiseasedCDReset.GetBool()) + Main.KilledDiseased.Clear(); + if (Options.AntidoteCDReset.GetBool()) + Main.KilledAntidote.Clear(); */ + + } + public static void AfterPlayerDeathTasks(PlayerControl target, bool onMeeting = false) + { + switch (target.GetCustomRole()) + { + case CustomRoles.Terrorist: + Logger.Info(target?.Data?.PlayerName + "はTerroristだった", "MurderPlayer"); + CheckTerroristWin(target.Data); + break; + case CustomRoles.Executioner: + if (Executioner.Target.ContainsKey(target.PlayerId)) + { + Executioner.Target.Remove(target.PlayerId); + Executioner.SendRPC(target.PlayerId); + } + break; + case CustomRoles.Lawyer: + if (Lawyer.Target.ContainsKey(target.PlayerId)) + { + Lawyer.Target.Remove(target.PlayerId); + Lawyer.SendRPC(target.PlayerId); + } + break; + case CustomRoles.CyberStar: + if (GameStates.IsMeeting) + { + //网红死亡消息提示 + foreach (var pc in Main.AllPlayerControls) + { + if (!Options.ImpKnowCyberStarDead.GetBool() && pc.GetCustomRole().IsImpostor()) continue; + if (!Options.NeutralKnowCyberStarDead.GetBool() && pc.GetCustomRole().IsNeutral()) continue; + SendMessage(string.Format(GetString("CyberStarDead"), target.GetRealName()), pc.PlayerId, ColorString(GetRoleColor(CustomRoles.CyberStar), GetString("CyberStarNewsTitle"))); + } + } + else + { + if (!Main.CyberStarDead.Contains(target.PlayerId)) + Main.CyberStarDead.Add(target.PlayerId); + } + break; + case CustomRoles.Pelican: + Pelican.OnPelicanDied(target.PlayerId); + break; + case CustomRoles.Devourer: + Devourer.OnDevourerDied(target.PlayerId); + break; + } + + if (Executioner.Target.ContainsValue(target.PlayerId)) + Executioner.ChangeRoleByTarget(target); + if (Lawyer.Target.ContainsValue(target.PlayerId)) + Lawyer.ChangeRoleByTarget(target); + + FixedUpdatePatch.LoversSuicide(target.PlayerId, onMeeting); + } + public static void ChangeInt(ref int ChangeTo, int input, int max) + { + var tmp = ChangeTo * 10; + tmp += input; + ChangeTo = Math.Clamp(tmp, 0, max); + } + public static void CountAlivePlayers(bool sendLog = false) + { + int AliveImpostorCount = Main.AllAlivePlayerControls.Count(pc => pc.Is(CustomRoleTypes.Impostor)); + if (Main.AliveImpostorCount != AliveImpostorCount) + { + Logger.Info("存活内鬼人数:" + AliveImpostorCount + "人", "CountAliveImpostors"); + Main.AliveImpostorCount = AliveImpostorCount; + LastImpostor.SetSubRole(); + } + + if (sendLog) + { + var sb = new StringBuilder(100); + foreach (var countTypes in Enum.GetValues(typeof(CountTypes)).Cast()) + { + var playersCount = PlayersCount(countTypes); + if (playersCount == 0) continue; + sb.Append($"{countTypes}:{AlivePlayersCount(countTypes)}/{playersCount}, "); + } + sb.Append($"All:{AllAlivePlayersCount}/{AllPlayersCount}"); + Logger.Info(sb.ToString(), "CountAlivePlayers"); + } + } + public static string GetVoteName(byte num) + { + string name = "invalid"; + var player = GetPlayerById(num); + if (num < 15 && player != null) name = player?.GetNameWithRole(); + if (num == 253) name = "Skip"; + if (num == 254) name = "None"; + if (num == 255) name = "Dead"; + return name; + } + public static string PadRightV2(this object text, int num) + { + int bc = 0; + var t = text.ToString(); + foreach (char c in t) bc += Encoding.GetEncoding("UTF-8").GetByteCount(c.ToString()) == 1 ? 1 : 2; + return t?.PadRight(Mathf.Max(num - (bc - t.Length), 0)); + } + public static void DumpLog() + { + string f = $"{Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)}/TOHE-logs/"; + string t = DateTime.Now.ToString("yyyy-MM-dd_HH.mm.ss"); + string filename = $"{f}TOHE-v{Main.PluginVersion}-{t}.log"; + if (!Directory.Exists(f)) Directory.CreateDirectory(f); + FileInfo file = new(@$"{Environment.CurrentDirectory}/BepInEx/LogOutput.log"); + file.CopyTo(@filename); + if (PlayerControl.LocalPlayer != null) + HudManager.Instance?.Chat?.AddChat(PlayerControl.LocalPlayer, string.Format(GetString("Message.DumpfileSaved"), $"TOHE - v{Main.PluginVersion}-{t}.log")); + System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo("Explorer.exe") + { Arguments = "/e,/select," + @filename.Replace("/", "\\") }; + System.Diagnostics.Process.Start(psi); + } + public static (int, int) GetDousedPlayerCount(byte playerId) + { + int doused = 0, all = 0; //学校で習った書き方 + //多分この方がMain.isDousedでforeachするより他のアーソニストの分ループ数少なくて済む + foreach (var pc in Main.AllAlivePlayerControls) + { + if (pc.PlayerId == playerId) continue; //塗れない人は除外 (死んでたり切断済みだったり あとアーソニスト自身も) + + all++; + if (Main.isDoused.TryGetValue((playerId, pc.PlayerId), out var isDoused) && isDoused) + //塗れている場合 + doused++; + } + + return (doused, all); + } + public static (int, int) GetDrawPlayerCount(byte playerId, out List winnerList) + { + int draw = 0; + int all = Options.RevolutionistDrawCount.GetInt(); + int max = Main.AllAlivePlayerControls.Count(); + if (!Main.PlayerStates[playerId].IsDead) max--; + winnerList = new(); + if (all > max) all = max; + foreach (var pc in Main.AllPlayerControls) + { + if (Main.isDraw.TryGetValue((playerId, pc.PlayerId), out var isDraw) && isDraw) + { + winnerList.Add(pc); + draw++; + } + } + return (draw, all); + } + public static string SummaryTexts(byte id, bool disableColor = true, bool check = false) + { + var RolePos = TranslationController.Instance.currentLanguage.languageID is SupportedLangs.English or SupportedLangs.Russian ? 37 : 34; + var KillsPos = TranslationController.Instance.currentLanguage.languageID is SupportedLangs.English or SupportedLangs.Russian ? 14 : 12; + var name = Main.AllPlayerNames[id].RemoveHtmlTags().Replace("\r\n", string.Empty); + if (id == PlayerControl.LocalPlayer.PlayerId) name = DataManager.player.Customization.Name; + else name = GetPlayerById(id)?.Data.PlayerName ?? name; + string summary = $"{ColorString(Main.PlayerColors[id], name)}{GetProgressText(id)} {GetKillCountText(id)} {GetVitalText(id, true)} {GetDisplayRoleName(id, true)}{GetSubRolesText(id, summary: true)}"; + if (Options.CurrentGameMode == CustomGameMode.SoloKombat) + { + if (TranslationController.Instance.currentLanguage.languageID is SupportedLangs.SChinese or SupportedLangs.TChinese) + summary = $"{GetProgressText(id)}\t{ColorString(Main.PlayerColors[id], name)}"; + else summary = $"{ColorString(Main.PlayerColors[id], name)}{GetProgressText(id)}"; + if (GetProgressText(id).Trim() == "") return "INVALID"; + } + return check && GetDisplayRoleName(id, true).RemoveHtmlTags().Contains("INVALID:NotAssigned") + ? "INVALID" + : disableColor ? summary.RemoveHtmlTags() : summary; + } + public static string RemoveHtmlTagsTemplate(this string str) => Regex.Replace(str, "", ""); + public static string RemoveHtmlTags(this string str) => Regex.Replace(str, "<[^>]*?>", ""); + public static bool CanMafiaKill() + { + if (Main.PlayerStates == null) return false; + //マフィアを除いた生きているインポスターの人数 Number of Living Impostors excluding mafia + int LivingImpostorsNum = 0; + foreach (var pc in Main.AllAlivePlayerControls) + { + var role = pc.GetCustomRole(); + if (role != CustomRoles.Mafia && role.IsImpostor()) LivingImpostorsNum++; + } + + return LivingImpostorsNum <= 0; + } + public static void FlashColor(Color color, float duration = 1f) + { + var hud = DestroyableSingleton.Instance; + if (hud.FullScreen == null) return; + var obj = hud.transform.FindChild("FlashColor_FullScreen")?.gameObject; + if (obj == null) + { + obj = UnityEngine.Object.Instantiate(hud.FullScreen.gameObject, hud.transform); + obj.name = "FlashColor_FullScreen"; + } + hud.StartCoroutine(Effects.Lerp(duration, new Action((t) => + { + obj.SetActive(t != 1f); + obj.GetComponent().color = new(color.r, color.g, color.b, Mathf.Clamp01((-2f * Mathf.Abs(t - 0.5f) + 1) * color.a / 2)); //アルファ値を0→目標→0に変化させる + }))); + } + + public static Dictionary CachedSprites = new(); + public static Sprite LoadSprite(string path, float pixelsPerUnit = 1f) + { + try + { + if (CachedSprites.TryGetValue(path + pixelsPerUnit, out var sprite)) return sprite; + Texture2D texture = LoadTextureFromResources(path); + sprite = Sprite.Create(texture, new(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), pixelsPerUnit); + sprite.hideFlags |= HideFlags.HideAndDontSave | HideFlags.DontSaveInEditor; + return CachedSprites[path + pixelsPerUnit] = sprite; + } + catch + { + Logger.Error($"读入Texture失败:{path}", "LoadImage"); + } + return null; + } + public static Texture2D LoadTextureFromResources(string path) + { + try + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); + using MemoryStream ms = new(); + stream.CopyTo(ms); + ImageConversion.LoadImage(texture, ms.ToArray(), false); + return texture; + } + catch + { + Logger.Error($"读入Texture失败:{path}", "LoadImage"); + } + return null; + } + public static string ColorString(Color32 color, string str) => $"{str}"; + /// + /// Darkness:1の比率で黒色と元の色を混ぜる。マイナスだと白色と混ぜる。 + /// + public static Color ShadeColor(this Color color, float Darkness = 0) + { + bool IsDarker = Darkness >= 0; //黒と混ぜる + if (!IsDarker) Darkness = -Darkness; + float Weight = IsDarker ? 0 : Darkness; //黒/白の比率 + float R = (color.r + Weight) / (Darkness + 1); + float G = (color.g + Weight) / (Darkness + 1); + float B = (color.b + Weight) / (Darkness + 1); + return new Color(R, G, B, color.a); + } + + /// + /// 乱数の簡易的なヒストグラムを取得する関数 + /// 生成した乱数を格納したint配列 + /// ヒストグラムの倍率 大量の乱数を扱う場合、この値を下げることをお勧めします。 + /// + public static string WriteRandomHistgram(int[] nums, float scale = 1.0f) + { + int[] countData = new int[nums.Max() + 1]; + foreach (var num in nums) + { + if (0 <= num) countData[num]++; + } + StringBuilder sb = new(); + for (int i = 0; i < countData.Length; i++) + { + // 倍率適用 + countData[i] = (int)(countData[i] * scale); + + // 行タイトル + sb.AppendFormat("{0:D2}", i).Append(" : "); + + // ヒストグラム部分 + for (int j = 0; j < countData[i]; j++) + sb.Append('|'); + + // 改行 + sb.Append('\n'); + } + + // その他の情報 + sb.Append("最大数 - 最小数: ").Append(countData.Max() - countData.Min()); + + return sb.ToString(); + } + + public static bool TryCast(this Il2CppObjectBase obj, out T casted) + where T : Il2CppObjectBase + { + casted = obj.TryCast(); + return casted != null; + } + public static int AllPlayersCount => Main.PlayerStates.Values.Count(state => state.countTypes != CountTypes.OutOfGame); + public static int AllAlivePlayersCount => Main.AllAlivePlayerControls.Count(pc => !pc.Is(CountTypes.OutOfGame)); + public static bool IsAllAlive => Main.PlayerStates.Values.All(state => state.countTypes == CountTypes.OutOfGame || !state.IsDead); + public static int PlayersCount(CountTypes countTypes) => Main.PlayerStates.Values.Count(state => state.countTypes == countTypes); + public static int AlivePlayersCount(CountTypes countTypes) => Main.AllAlivePlayerControls.Count(pc => pc.Is(countTypes)); +} \ No newline at end of file diff --git a/Modules/Zoom.cs b/Modules/Zoom.cs new file mode 100644 index 000000000..5a024727a --- /dev/null +++ b/Modules/Zoom.cs @@ -0,0 +1,104 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace TOHE; + +// 来源:https://github.com/tugaru1975/TownOfPlus/TOPmods/Zoom.cs +// 参考:https://github.com/Yumenopai/TownOfHost_Y +[HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] +public static class Zoom +{ + private static bool ResetButtons = false; + public static void Postfix() + { + //if (PlayerControl.LocalPlayer.Is(RoleType.Impostor) && Options.OperateVisibilityImpostor.GetBool()) return; + if (GameStates.IsShip && !GameStates.IsMeeting && GameStates.IsCanMove && PlayerControl.LocalPlayer.Data.IsDead || GameStates.IsLobby && GameStates.IsCanMove) + { + if (Camera.main.orthographicSize > 3.0f) + ResetButtons = true; + + if (Input.mouseScrollDelta.y > 0) + { + if (Camera.main.orthographicSize > 3.0f) + { + SetZoomSize(times: false); + } + + } + if (Input.mouseScrollDelta.y < 0) + { + if (GameStates.IsDead || GameStates.IsFreePlay || DebugModeManager.AmDebugger || GameStates.IsLobby || + PlayerControl.LocalPlayer.FriendCode.GetDevUser().DeBug) + { + if (Camera.main.orthographicSize < 18.0f) + { + SetZoomSize(times: true); + } + } + } + Flag.NewFlag("Zoom"); + } + else + { + Flag.Run(() => + { + SetZoomSize(reset: true); + }, "Zoom"); + } + } + + private static void SetZoomSize(bool times = false, bool reset = false) + { + var size = 1.5f; + if (!times) size = 1 / size; + if (reset) + { + Camera.main.orthographicSize = 3.0f; + HudManager.Instance.UICamera.orthographicSize = 3.0f; + HudManager.Instance.Chat.transform.localScale = Vector3.one; + if (GameStates.IsMeeting) MeetingHud.Instance.transform.localScale = Vector3.one; + } + else + { + Camera.main.orthographicSize *= size; + HudManager.Instance.UICamera.orthographicSize *= size; + } + DestroyableSingleton.Instance?.ShadowQuad?.gameObject?.SetActive((reset || Camera.main.orthographicSize == 3.0f) && PlayerControl.LocalPlayer.IsAlive()); + + if (ResetButtons) + { + ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen); + ResetButtons = false; + } + } + + public static void OnFixedUpdate() + => DestroyableSingleton.Instance?.ShadowQuad?.gameObject?.SetActive((Camera.main.orthographicSize == 3.0f) && PlayerControl.LocalPlayer.IsAlive()); +} + +public static class Flag +{ + private static List OneTimeList = new(); + private static List FirstRunList = new(); + public static void Run(Action action, string type, bool firstrun = false) + { + if (OneTimeList.Contains(type) || (firstrun && !FirstRunList.Contains(type))) + { + if (!FirstRunList.Contains(type)) FirstRunList.Add(type); + OneTimeList.Remove(type); + action(); + } + + } + public static void NewFlag(string type) + { + if (!OneTimeList.Contains(type)) OneTimeList.Add(type); + } + + public static void DeleteFlag(string type) + { + if (OneTimeList.Contains(type)) OneTimeList.Remove(type); + } +} \ No newline at end of file