diff --git a/CTA/CustomTeamAssigner/Utils.cs b/CTA/CustomTeamAssigner/Utils.cs index 53a84c035..adf5d0662 100644 --- a/CTA/CustomTeamAssigner/Utils.cs +++ b/CTA/CustomTeamAssigner/Utils.cs @@ -27,7 +27,6 @@ namespace CustomTeamAssigner { CustomRoles.Sans, "Arrogance" }, { CustomRoles.OverKiller, "Butcher" }, { CustomRoles.EvilDiviner, "Consigliere" }, - { CustomRoles.Minimalism, "Killing Machine" }, { CustomRoles.BallLightning, "Lightning" }, { CustomRoles.LovingCrewmate, "Lover" }, { CustomRoles.LovingImpostor, "Loving Impostor" }, diff --git a/CustomRoles.cs b/CustomRoles.cs index a5f9ff0bd..110670971 100644 --- a/CustomRoles.cs +++ b/CustomRoles.cs @@ -73,6 +73,7 @@ public enum CustomRoles KillingMachine, BallLightning, // Lightning Librarian, + Loner, Lurker, Mafioso, Mastermind, diff --git a/Modules/AntiBlackout.cs b/Modules/AntiBlackout.cs index 286764db0..9996919f9 100644 --- a/Modules/AntiBlackout.cs +++ b/Modules/AntiBlackout.cs @@ -24,7 +24,7 @@ public static class AntiBlackout PlayerControl[] players = Main.AllAlivePlayerControls; if (CheckForEndVotingPatch.TempExiledPlayer != null) players = players.Where(x => x.PlayerId != CheckForEndVotingPatch.TempExiledPlayer.PlayerId).ToArray(); - PlayerControl dummyImp = players.MinBy(x => x.PlayerId); + PlayerControl dummyImp = players.OrderByDescending(x => x.GetRoleMap().RoleType != RoleTypes.Detective).ThenByDescending(x => x.IsModdedClient()).MinBy(x => x.PlayerId); if (players.Length == 2) { @@ -135,7 +135,7 @@ public static class AntiBlackout { SkipTasks = false; ExileControllerWrapUpPatch.AfterMeetingTasks(); - }, Math.Min(3f, Math.Max(1f, Utils.CalculatePingDelay() * 2f)), "Reset SkipTasks after SetRealPlayerRoles"); + }, 1f, "Reset SkipTasks after SetRealPlayerRoles"); }, 0.2f, "SetRealPlayerRoles - Reset Cooldowns"); } diff --git a/Modules/CustomRolesHelper.cs b/Modules/CustomRolesHelper.cs index 71cd63544..edc9a7d41 100644 --- a/Modules/CustomRolesHelper.cs +++ b/Modules/CustomRolesHelper.cs @@ -152,6 +152,7 @@ internal static class CustomRolesHelper CustomRoles.Forger => CustomRoles.Impostor, CustomRoles.ClockBlocker => CustomRoles.Impostor, CustomRoles.Psychopath => CustomRoles.Impostor, + CustomRoles.Loner => CustomRoles.Impostor, CustomRoles.Postponer => CustomRoles.Impostor, CustomRoles.Venerer => UsePets ? CustomRoles.Impostor : CustomRoles.Shapeshifter, CustomRoles.Kidnapper => CustomRoles.Shapeshifter, @@ -1684,6 +1685,7 @@ internal static class CustomRolesHelper CustomRoles.EvilTracker => RoleOptionType.Impostor_Miscellaneous, CustomRoles.Gambler => RoleOptionType.Impostor_Miscellaneous, CustomRoles.Generator => RoleOptionType.Impostor_Miscellaneous, + CustomRoles.Loner => RoleOptionType.Impostor_Miscellaneous, CustomRoles.Visionary => RoleOptionType.Impostor_Miscellaneous, CustomRoles.Wildling => RoleOptionType.Impostor_Miscellaneous, CustomRoles.Impostor => RoleOptionType.Impostor_Miscellaneous, diff --git a/Modules/ExtendedPlayerControl.cs b/Modules/ExtendedPlayerControl.cs index b69a625d6..b21ec2995 100644 --- a/Modules/ExtendedPlayerControl.cs +++ b/Modules/ExtendedPlayerControl.cs @@ -755,7 +755,7 @@ internal static class ExtendedPlayerControl LateTask.New(() => { Main.PlayerStates[player.PlayerId].IsBlackOut = false; // Cancel blackout - player.MarkDirtySettings(); + player.SyncSettings(); }, duration, "RemoveKillFlash"); if (player.AmOwner) @@ -770,7 +770,7 @@ internal static class ExtendedPlayerControl } else if (!reactorCheck) player.ReactorFlash(); // Reactor flash - player.MarkDirtySettings(); + player.SyncSettings(); } public static void RpcGuardAndKill(this PlayerControl killer, PlayerControl target = null, bool forObserver = false, bool fromSetKCD = false) @@ -1240,12 +1240,12 @@ internal static class ExtendedPlayerControl if (IsActive(systemtypes)) { Main.PlayerStates[pc.PlayerId].IsBlackOut = true; - pc.MarkDirtySettings(); + pc.SyncSettings(); LateTask.New(() => { Main.PlayerStates[pc.PlayerId].IsBlackOut = false; - pc.MarkDirtySettings(); + pc.SyncSettings(); }, (float.IsNaN(flashDuration) ? Options.KillFlashDuration.GetFloat() : flashDuration) + delay, "Fix BlackOut Reactor Flash"); return; diff --git a/Modules/OptionHolder.cs b/Modules/OptionHolder.cs index 85b65a3e0..dae9daafa 100644 --- a/Modules/OptionHolder.cs +++ b/Modules/OptionHolder.cs @@ -1168,6 +1168,8 @@ public static class Options File.WriteAllText(path + "/friendcode#1234.txt", JsonSerializer.Serialize(new UserData(), new JsonSerializerOptions { WriteIndented = true })); } + List errors = []; + foreach (string file in Directory.GetFiles(path, "*.txt")) { try @@ -1180,10 +1182,15 @@ public static class Options } catch (Exception e) { - Logger.Error($"Failed to load user data from {file}", "Options"); - Utils.ThrowException(e); + errors.Add($"{file}: {e.Message}"); } } + + if (errors.Count > 0) + { + errors.Insert(0, "The following errors occurred while loading user data files:"); + Logger.Error(string.Join('\n', errors), "Options", multiLine: true); + } } catch (Exception e) { Utils.ThrowException(e); } } diff --git a/Patches/ExilePatch.cs b/Patches/ExilePatch.cs index 8d77d2104..b0c5a05ca 100644 --- a/Patches/ExilePatch.cs +++ b/Patches/ExilePatch.cs @@ -126,7 +126,7 @@ internal static class ExileControllerWrapUpPatch { if (GameStates.IsEnded) return; AntiBlackout.RevertToActualRoleTypes(); - }, Math.Max(1f, Utils.CalculatePingDelay()), "Revert AntiBlackout Measures"); + }, 2f, "Revert AntiBlackout Measures"); } GameStates.AlreadyDied |= !Utils.IsAllAlive; diff --git a/Patches/MeetingHudPatch.cs b/Patches/MeetingHudPatch.cs index 4a8bc4887..0f415278b 100644 --- a/Patches/MeetingHudPatch.cs +++ b/Patches/MeetingHudPatch.cs @@ -995,7 +995,7 @@ internal static class MeetingHudStartPatch { var sender = CustomRpcSender.Create($"RpcSetRoleDesync for meeting shapeshift ({Main.AllPlayerNames.GetValueOrDefault(pc.PlayerId, "Someone")})", SendOption.Reliable); sender.RpcSetRole(pc, RoleTypes.Shapeshifter, pc.OwnerId); - aapc.DoIf(x => x.IsImpostor(), x => sender.RpcSetRole(x, RoleTypes.Crewmate, pc.OwnerId)); + if (!pc.IsImpostor()) aapc.DoIf(x => x.IsImpostor(), x => sender.RpcSetRole(x, RoleTypes.Crewmate, pc.OwnerId)); sender.SendMessage(); } } diff --git a/Patches/PlayerControlPatch.cs b/Patches/PlayerControlPatch.cs index e66bdc80f..d02b53792 100644 --- a/Patches/PlayerControlPatch.cs +++ b/Patches/PlayerControlPatch.cs @@ -1203,6 +1203,11 @@ internal static class ReportDeadBodyPatch if (MeetingStarted) return; MeetingStarted = true; LateTask.New(() => MeetingStarted = false, 1f, "ResetMeetingStarted"); + + Main.PlayerStates.Values.DoIf(x => !x.IsDead, x => x.IsBlackOut = true); + Main.AllAlivePlayerControls.Do(x => x.SyncSettings()); + + LateTask.New(() => Main.PlayerStates.Values.Do(x => x.IsBlackOut = false), 3f, "RemoveBlackout"); CustomNetObject.OnMeeting(); diff --git a/Roles/Crewmate/Astral.cs b/Roles/Crewmate/Astral.cs index 70bdde80b..819b03eca 100644 --- a/Roles/Crewmate/Astral.cs +++ b/Roles/Crewmate/Astral.cs @@ -74,17 +74,25 @@ public class Astral : RoleBase pc.Exiled(); CustomRpcSender.Create("Astral", (SendOption)1).AutoStartRpc(pc.NetId, 4).EndRpc().SendMessage(); - LateTask.New(() => pc.RpcSetRoleDesync(RoleTypes.GuardianAngel, pc.OwnerId), 0.2f, log: false); - LateTask.New(pc.RpcResetAbilityCooldown, 0.4f, log: false); - LateTask.New(() => - { - if (ReportDeadBodyPatch.MeetingStarted || GameStates.IsMeeting) return; - pc.SetChatVisible(false); - }, 0.6f, log: false); + Main.Instance.StartCoroutine(CoRoutine()); pc.MarkDirtySettings(); BackTS = Utils.TimeStamp + AbilityDuration.GetInt() + 1; Utils.SendRPC(CustomRPC.SyncRoleData, AstralId, BackTS); + return; + + System.Collections.IEnumerator CoRoutine() + { + yield return new WaitForSeconds(0.2f); + if (ReportDeadBodyPatch.MeetingStarted || GameStates.IsMeeting) yield break; + pc.RpcSetRoleDesync(RoleTypes.GuardianAngel, pc.OwnerId); + yield return new WaitForSeconds(0.2f); + if (ReportDeadBodyPatch.MeetingStarted || GameStates.IsMeeting) yield break; + pc.RpcResetAbilityCooldown(); + yield return new WaitForSeconds(0.2f); + if (ReportDeadBodyPatch.MeetingStarted || GameStates.IsMeeting) yield break; + pc.SetChatVisible(false); + } } void BecomeAliveAgain(PlayerControl pc, bool onMeeting = false) diff --git a/Roles/Impostor/Loner.cs b/Roles/Impostor/Loner.cs new file mode 100644 index 000000000..c3d04bab0 --- /dev/null +++ b/Roles/Impostor/Loner.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using EHR.Modules; +using EHR.Neutral; + +namespace EHR.Impostor; + +public class Loner : RoleBase +{ + public static bool On; + + public override bool IsEnable => On; + + public byte PickedPlayer; + public CustomRoles PickedRole; + + public override void SetupCustomOption() + { + StartSetup(655300); + } + + public override void Init() + { + On = false; + } + + public override void Add(byte playerId) + { + On = true; + PickedPlayer = byte.MaxValue; + PickedRole = default; + } + + public override void OnMeetingShapeshift(PlayerControl shapeshifter, PlayerControl target) + { + if (Starspawn.IsDayBreak) return; + if (shapeshifter == null || target == null || shapeshifter.PlayerId == target.PlayerId || PickedPlayer != byte.MaxValue) return; + + PickedPlayer = target.PlayerId; + PickedRole = Enum.GetValues().Where(x => x.IsImpostor() && !x.IsVanilla() && !CustomRoleSelector.RoleResult.ContainsValue(x) && x.GetMode() != 0).RandomElement(); + + Utils.SendMessage("\n", shapeshifter.PlayerId, string.Format(Translator.GetString("Loner.Picked"), PickedPlayer.ColoredPlayerName(), PickedRole.ToColoredString())); + } +} \ No newline at end of file