Files
EndlessHostRoles/Patches/PlayerJoinAndLeftPatch.cs
T
2026-09-24 20:26:16 +02:00

913 lines
39 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using AmongUs.Data;
#if IL2CPP
using AmongUs.GameOptions;
#endif
using AmongUs.InnerNet.GameDataMessages;
using EHR.Gamemodes;
using EHR.Modules;
using EHR.Modules.Extensions;
using EHR.Patches;
using EHR.Roles;
using HarmonyLib;
using Hazel;
using InnerNet;
using UnityEngine;
using static EHR.Translator;
namespace EHR;
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameJoined))]
internal static class OnGameJoinedPatch
{
public static bool JoiningGame;
private static bool ClearedLogs;
public static void Postfix(AmongUsClient __instance)
{
JoiningGame = true;
Logger.Info($"{__instance.GameId} joined lobby", "OnGameJoined");
SetUpRoleTextPatch.IsInIntro = false;
Main.PlayerVersion = [];
RPC.RpcVersionCheck();
SoundManager.Instance?.ChangeAmbienceVolume(DataManager.Settings.Audio.AmbienceVolume);
Options.LoadUserData();
GameStates.InGame = false;
ErrorText.Instance?.Clear();
ChatCommands.VotedToStart = [];
Main.GameTimer.Reset();
Utils.DirtyName = [];
LateTask.New(Achievements.ShowWaitingAchievements, 8f, log: false);
if (!ClearedLogs)
{
LateTask.New(() =>
{
if (!HudManager.InstanceExists || AmongUsClient.Instance.IsGameStarted) return;
(int Files, int Folders) result = CleanOldItems();
if (result.Files > 0 || result.Folders > 0)
{
Prompt.Show(string.Format(GetString("Promt.DeleteOldLogs"), result.Files, result.Folders), () =>
{
result = CleanOldItems(false);
LateTask.New(() => HudManager.Instance.ShowPopUp(string.Format(GetString("LogDeletionResults"), result.Files, result.Folders)), 0.01f);
}, () => { });
}
ClearedLogs = true;
}, 5f, log: false);
}
if (AmongUsClient.Instance.AmHost)
{
GameStartManagerPatch.GameStartManagerUpdatePatch.ExitTimer = -1;
Main.DoBlockNameChange = false;
EAC.DeNum = 0;
Main.AllPlayerNames = [];
Main.AllClientRealNames = [];
if (Main.NormalOptions?.KillCooldown == 0f) Main.NormalOptions.KillCooldown = Main.LastKillCooldown.Value;
#if IL2CPP
AURoleOptions.SetOpt(Main.NormalOptions.CastFast<IGameOptions>());
#else
AURoleOptions.SetOpt(Main.NormalOptions);
#endif
if (AURoleOptions.ShapeshifterCooldown == 0f) AURoleOptions.ShapeshifterCooldown = Main.LastShapeshifterCooldown.Value;
LateTask.New(() =>
{
JoiningGame = false;
Main.RealOptionsData?.Restore(GameOptionsManager.Instance.CurrentGameOptions);
Options.AutoSetFactionMinMaxSettings();
try
{
ClientData client = PlayerControl.LocalPlayer.GetClient();
if (BanManager.CheckEACList(PlayerControl.LocalPlayer.FriendCode, client.GetHashedPuid()) && GameStates.IsOnlineGame)
{
AmongUsClient.Instance.ExitGame(DisconnectReasons.Banned);
SceneChanger.ChangeScene("MainMenu");
}
Logger.Info($"{client.PlayerName.RemoveHtmlTags()} (ClientID: {client.Id} / FriendCode: {client.FriendCode} / HashPuid: {client.GetHashedPuid()} / Platform: {client.PlatformData.Platform}) Hosted room (Server: {Utils.GetRegionName()})", "Session");
}
catch (Exception e) { Logger.Error($"ClientData null:{PlayerControl.LocalPlayer?.GetClient() == null}, PlayerControl.LocalPlayer null:{!PlayerControl.LocalPlayer} - {e}", "OnGameJoinedPatch"); }
//Main.Instance.StartCoroutine(OptionShower.GetText());
}, 2f, "OnGameJoinedPatch");
LateTask.New(() =>
{
if (Main.NormalOptions != null && Mathf.Approximately(Main.NormalOptions.KillCooldown, 25f))
Main.NormalOptions.KillCooldown = Options.FallBackKillCooldownValue?.GetFloat() ?? 25f;
}, 5f, log: false);
Main.SetRoles = [];
Main.SetAddOns = [];
ChatCommands.DraftResult = [];
ChatCommands.DraftRoles = [];
LateTask.New(() =>
{
if (GameStates.CurrentServerType is not GameStates.ServerType.Custom and not GameStates.ServerType.Local)
{
try
{
LobbySharingAPI.NotifyLobbyStatusChanged(LobbyStatus.In_Lobby);
if (GameStates.InGame) LateTask.New(() => LobbySharingAPI.NotifyLobbyStatusChanged(LobbyStatus.In_Game), 5f, "NotifyLobbyStatusChanged Immediately");
}
catch (Exception e) { Utils.ThrowException(e); }
}
else Logger.Info($"Not sending lobby status to the server because the server type is {GameStates.CurrentServerType}", "OnGameJoinedPatch");
}, 5f, "NotifyLobbyCreated");
if (Options.AutoGMPollCommandAfterJoin.GetBool() && !Options.AutoGMRotationEnabled)
{
Main.Instance.StartCoroutine(CoRoutine());
IEnumerator CoRoutine()
{
float timer = Options.AutoGMPollCommandCooldown.GetInt();
while (timer > 0)
{
if (!GameStates.IsLobby) yield break;
timer -= Time.deltaTime;
yield return null;
}
if (Options.AutoGMPollCommandAfterJoin.GetBool() && !Options.AutoGMRotationEnabled)
ChatCommands.GameModePollCommand(PlayerControl.LocalPlayer, "/gmpoll", ["/gmpoll"]);
}
}
if (Options.AutoMPollCommandAfterJoin.GetBool() && !Options.RandomMapsMode.GetBool())
{
Main.Instance.StartCoroutine(CoRoutine());
IEnumerator CoRoutine()
{
float timer = Options.AutoMPollCommandCooldown.GetInt();
while (timer > 0)
{
if (!GameStates.IsLobby) yield break;
timer -= Time.deltaTime;
yield return null;
}
if (Options.AutoMPollCommandAfterJoin.GetBool() && !Options.RandomMapsMode.GetBool())
ChatCommands.MapPollCommand(PlayerControl.LocalPlayer, "/mpoll", ["/mpoll"]);
}
}
if (Options.AutoDraftStartCommandAfterJoin.GetBool())
{
Main.Instance.StartCoroutine(CoRoutine());
IEnumerator CoRoutine()
{
float timer = Options.AutoDraftStartCommandCooldown.GetInt();
while (timer > 0)
{
if (!GameStates.IsLobby) yield break;
timer -= Time.deltaTime;
yield return null;
}
if (Options.AutoDraftStartCommandAfterJoin.GetBool())
ChatCommands.DraftStartCommand(PlayerControl.LocalPlayer, "/draftstart", ["/draftstart"]);
}
}
if (Options.AutoReadyCheckCommandAfterJoin.GetBool())
{
Main.Instance.StartCoroutine(CoRoutine());
IEnumerator CoRoutine()
{
float timer = Options.AutoReadyCheckCommandCooldown.GetInt();
while (timer > 0)
{
if (!GameStates.IsLobby) yield break;
timer -= Time.deltaTime;
yield return null;
}
if (Options.AutoReadyCheckCommandAfterJoin.GetBool())
ChatCommands.ReadyCheckCommand(PlayerControl.LocalPlayer, "/readycheck", ["/readycheck"]);
}
}
if (Options.AutoGMRotationEnabled)
{
Main.Instance.StartCoroutine(CoRoutine());
IEnumerator CoRoutine()
{
yield return new WaitForSecondsRealtime(10f);
try { Utils.SendMessage(HudManagerPatch.BuildAutoGMRotationStatusText(true), title: GetString("AutoGMRotationStatusText")); }
catch (Exception e) { Utils.ThrowException(e); }
CustomGameMode nextGM = Options.AutoGMRotationCompiled[Options.AutoGMRotationIndex];
float timer;
if (nextGM != CustomGameMode.All) timer = 0f;
else if (Options.AutoGMPollCommandAfterJoin.GetBool()) timer = Options.AutoGMPollCommandCooldown.GetInt() - 10;
else if (Main.AutoStart.Value) timer = Options.MinWaitAutoStart.GetFloat() * 60 - 65;
else timer = 30f;
Logger.Info($"Auto GM Rotation timer: {timer}", "Auto GM Rotation");
HudManagerPatch.AutoGMRotationCooldownTimerEndTS = Utils.TimeStamp + (int)timer;
while (timer > 0)
{
if (!GameStates.IsLobby) yield break;
timer -= Time.deltaTime;
yield return null;
}
if (Options.AutoGMRotationEnabled)
{
if (nextGM == CustomGameMode.All) ChatCommands.GameModePollCommand(PlayerControl.LocalPlayer, "/gmpoll", ["/gmpoll"]);
else Options.GameMode.SetValue((int)nextGM - 1);
Logger.Info($"Auto GM Rotation: Next Game Mode = {nextGM}", "Auto GM Rotation");
}
}
}
}
//else
// LateTask.New(() => Main.Instance.StartCoroutine(OptionShower.GetText()), 10f, "OptionShower.GetText on client");
}
// Written with AI because I don't want it to delete the wrong files
/// <summary>
/// Cleans files and folders older than `days` in the EHR_Logs folder.
/// Default: dryRun = true (shows what would be deleted).
/// </summary>
private static (int Files, int Folders) CleanOldItems(bool dryRun = true, int days = 7)
{
if (OperatingSystem.IsAndroid()) return (0, 0); // not supported on starlight until we find a working solution for android
string path;
try
{
var f = Path.Combine(OperatingSystem.IsAndroid() ? Main.DataPath : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "EHR_Logs");
// Normalize separators
path = Path.GetFullPath(f);
}
catch (Exception ex)
{
Logger.Error($"Could not determine path: {ex.Message}", "CleanOldItems");
return (0, 0);
}
if (!Path.IsPathRooted(path))
{
Logger.Error("Target path is not rooted. Aborting for safety.", "CleanOldItems");
return (0, 0);
}
// Safety checks: ensure target folder name is exactly "EHR_Logs"
string folderName = Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
if (!string.Equals(folderName, "EHR_Logs", StringComparison.OrdinalIgnoreCase))
{
Logger.Error($"[ERROR] Target folder name is '{folderName}' (expected 'EHR_Logs'). Aborting for safety.", "CleanOldItems");
return (0, 0);
}
if (!Directory.Exists(path))
{
Logger.Msg("[INFO] Target directory does not exist. Nothing to clean.", "CleanOldItems");
return (0, 0);
}
DateTime thresholdUtc = DateTime.UtcNow - TimeSpan.FromDays(days);
Logger.Msg($"Threshold (UTC): delete items last written before {thresholdUtc:O}", "CleanOldItems");
Logger.Msg(dryRun
? "Running in dry run mode — no files or folders will be deleted."
: "Running for real — files and folders will be deleted.", "CleanOldItems");
var filesDeleted = 0;
var foldersDeleted = 0;
var failedDeletes = new List<string>();
// 1) Delete files older than threshold (walk all files recursively)
try
{
foreach (string file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
try
{
DateTime lastWriteUtc = File.GetLastWriteTimeUtc(file);
if (lastWriteUtc < thresholdUtc)
{
Logger.Warn(dryRun
? $"[DRY] Would delete file: {file} (LastWriteUtc: {lastWriteUtc:O})"
: $"[DEL] Deleting file: {file} (LastWriteUtc: {lastWriteUtc:O})", "CleanOldItems");
if (!dryRun)
{
File.SetAttributes(file, FileAttributes.Normal); // remove read-only to avoid exceptions
File.Delete(file);
}
// keep dry-run count behavior (so the user sees how many would be affected)
filesDeleted++;
}
}
catch (Exception exFile)
{
var msg = $"Failed to delete file '{file}': {exFile.Message}";
Logger.Error(msg, "CleanOldItems");
failedDeletes.Add(msg);
// continue with other files
}
}
}
catch (Exception exEnum) { Logger.Error($"Failed enumerating files: {exEnum.Message}", "CleanOldItems"); }
// 2) Attempt to delete directories that are empty AND older than threshold.
// We process directories from deepest to shallowest so we can remove empty parent dirs.
try
{
List<string> allDirectories = Directory
.EnumerateDirectories(path, "*", SearchOption.AllDirectories)
// compute depth in a robust way: count directory separators for the platform
.OrderByDescending(d => d.Count(c => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar))
.ToList();
foreach (string dir in allDirectories)
{
try
{
// re-check emptiness at time of delete: top-level only to avoid O(n^2)
bool hasEntries = Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.TopDirectoryOnly).Any();
if (hasEntries)
{
// Diagnostic: list top-level entries so we can see hidden/system files
try
{
List<string> entries = Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.TopDirectoryOnly).Take(10).ToList();
Logger.Msg($"Skipping non-empty dir: {dir}. Top-level entries (up to 10): {string.Join(", ", entries)}", "CleanOldItems");
}
catch { Logger.Msg($"Skipping non-empty dir: {dir}. Failed to list entries (possible permission issue).", "CleanOldItems"); }
continue;
}
DateTime lastWriteUtc = Directory.GetLastWriteTimeUtc(dir);
DateTime creationUtc = Directory.GetCreationTimeUtc(dir);
// Only delete if the directory is older than threshold by last write OR creation time (safer)
if (lastWriteUtc < thresholdUtc || creationUtc < thresholdUtc)
{
Logger.Warn(dryRun
? $"[DRY] Would delete empty folder: {dir} (LastWriteUtc: {lastWriteUtc:O}, CreationUtc: {creationUtc:O})"
: $"[DEL] Deleting empty folder: {dir} (LastWriteUtc: {lastWriteUtc:O}, CreationUtc: {creationUtc:O})", "CleanOldItems");
if (!dryRun)
{
try
{
Directory.Delete(dir, false); // false -> directory must be empty
foldersDeleted++;
}
catch (Exception exDel)
{
var msg = $"Failed to delete directory '{dir}': {exDel.Message}";
Logger.Error(msg, "CleanOldItems");
failedDeletes.Add(msg);
// Diagnostic: if delete failed because directory not empty, log the top-level entries
try
{
List<string> entries = Directory.EnumerateFileSystemEntries(dir, "*", SearchOption.TopDirectoryOnly).Take(10).ToList();
Logger.Msg($"Directory '{dir}' appears non-empty during deletion. Top-level entries (up to 10): {string.Join(", ", entries)}", "CleanOldItems");
}
catch
{
/* ignore */
}
}
}
else
{
// dry-run: increment the folder count to show how many *would* be deleted
foldersDeleted++;
}
}
}
catch (Exception exDir)
{
var msg = $"Failed processing directory '{dir}': {exDir.Message}";
Logger.Error(msg, "CleanOldItems");
failedDeletes.Add(msg);
}
}
// Optionally, check the root target folder itself: if empty and old, you might want to delete it.
// Here we will NOT delete the root EHR_Logs folder itself to be extra safe.
}
catch (Exception exEnumDirs) { Logger.Error($"Failed enumerating directories: {exEnumDirs.Message}", "CleanOldItems"); }
// Summary
Logger.Msg("=== Summary ===", "CleanOldItems");
Logger.Msg($"Files matched and processed: {filesDeleted}", "CleanOldItems");
Logger.Msg($"Folders matched and processed: {foldersDeleted}", "CleanOldItems");
if (failedDeletes.Count > 0)
{
Logger.Msg($"Failures ({failedDeletes.Count}):", "CleanOldItems");
foreach (string f in failedDeletes) Logger.Msg(f, "CleanOldItems");
}
else
Logger.Msg("No failures reported.", "CleanOldItems");
if (dryRun)
Logger.Msg("Dry run complete.", "CleanOldItems");
return (filesDeleted, foldersDeleted);
}
}
[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.DisconnectInternal))]
internal static class DisconnectInternalPatch
{
public static void Prefix( /*InnerNetClient __instance,*/ DisconnectReasons reason, string stringReason)
{
//ShowDisconnectPopupPatch.Reason = reason;
//ShowDisconnectPopupPatch.StringReason = stringReason;
ErrorText.Instance.CheatDetected = false;
ErrorText.Instance.SBDetected = false;
ErrorText.Instance.Clear();
}
}
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerJoined))]
internal static class OnPlayerJoinedPatch
{
private static CountdownTimer RpcSetNameWaitTimer;
public static bool IsDisconnected(this ClientData client)
{
foreach (ClientData clientData in AmongUsClient.Instance.allClients)
{
if (clientData.Id == client.Id)
return false;
}
return true;
}
public static void Postfix( /*AmongUsClient __instance,*/ [HarmonyArgument(0)] ClientData client)
{
Logger.Info($"{client.PlayerName} (ClientID: {client.Id} / FriendCode: {client.FriendCode} / Hashed PUID: {client.GetHashedPuid()}) joined the lobby", "Session");
Main.SetDirtyRebuildPC();
LateTask.New(() =>
{
try
{
if (!AmongUsClient.Instance.AmHost) return;
if (Options.KickSlowJoiningPlayers.GetBool() && (!client.IsDisconnected() && client.Character.Data.IsIncomplete || (client.Character.Data.DefaultOutfit.ColorId < 0 || Palette.PlayerColors.Length <= client.Character.Data.DefaultOutfit.ColorId) && PlayerControl.AllPlayerControls.Count <= 15))
{
Logger.SendInGame(GetString("Error.InvalidColor") + $" {client.Id}/{client.PlayerName}", Color.yellow);
AmongUsClient.Instance.KickPlayer(client.Id, false);
Logger.Info($"Kicked client {client.Id}/{client.PlayerName} since its PlayerControl was not spawned in time.", "OnPlayerJoinedPatchPostfix");
return;
}
if (client.Character && client.Character.Data && (client.Character.Data.DefaultOutfit.ColorId < 0 || Palette.PlayerColors.Length <= client.Character.Data.DefaultOutfit.ColorId) && PlayerControl.AllPlayerControls.Count >= 17)
Disco.ChangeColor(client.Character);
}
catch { }
}, 4.5f, "green bean kick late task", false);
if (AmongUsClient.Instance.AmHost && !client.ProductUserId.IsNullOrWhiteSpace() && client.ProductUserId.Length == 32 && client.FriendCode == "" && Options.KickPlayerFriendCodeNotExist.GetBool() && GameStates.CurrentServerType is not GameStates.ServerType.Local)
{
if (!BanManager.TempBanWhiteList.Contains(client.GetHashedPuid())) BanManager.TempBanWhiteList.Add(client.GetHashedPuid());
AmongUsClient.Instance.KickPlayer(client.Id, false);
Logger.SendInGame(string.Format(GetString("Message.KickedByNoFriendCode"), client.PlayerName), Color.yellow);
Logger.Info($"TempBanned a player {client.PlayerName} without a friend code", "Temp Ban");
}
if (AmongUsClient.Instance.AmHost && client.PlatformData.Platform is Platforms.Android or Platforms.IPhone && Options.KickMobilePlayer.GetBool())
{
AmongUsClient.Instance.KickPlayer(client.Id, false);
string msg = string.Format(GetString("KickMobilePlayer"), client.PlayerName);
Logger.SendInGame(msg, Color.yellow);
Logger.Info(msg, "Android Kick");
}
if (FriendsListManager.InstanceExists && !client.FriendCode.IsNullOrWhiteSpace() && FriendsListManager.Instance.IsPlayerBlockedUsername(client.FriendCode) && AmongUsClient.Instance.AmHost && GameStates.CurrentServerType is not GameStates.ServerType.Local)
{
AmongUsClient.Instance.KickPlayer(client.Id, true);
Logger.Info($"Blocked Player {client.PlayerName}({client.FriendCode}) has been banned.", "BAN");
}
BanManager.CheckBanPlayer(client);
if (AmongUsClient.Instance.AmHost)
{
Main.SayStartTimes.Remove(client.Id);
Main.SayBanwordsTimes.Remove(client.Id);
if (GameStates.IsLobby && !OnGameJoinedPatch.JoiningGame)
LateTask.New(Options.AutoSetFactionMinMaxSettings, 2f, log: false);
RpcSetNameWaitTimer?.Dispose();
RpcSetNameWaitTimer = new CountdownTimer(4f, () =>
{
RpcSetNameWaitTimer = null;
if (AmongUsClient.Instance.IsGameStarted) return;
Utils.ApplySuffix(PlayerControl.LocalPlayer, out string name);
PlayerControl.LocalPlayer.RpcSetName(name);
}, cancelOnMeeting: false, cancelOnGameEnd: false);
}
}
}
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))]
internal static class OnPlayerLeftPatch
{
public static void Postfix(AmongUsClient __instance, [HarmonyArgument(0)] ClientData data, [HarmonyArgument(1)] DisconnectReasons reason)
{
try
{
Main.SetDirtyRebuildPC();
if (AmongUsClient.Instance.AmHost && GameStates.IsInGame && data != null && data.Character)
{
byte id = data.Character.PlayerId;
ExtendedPlayerControl.TempExiled.Remove(id);
switch (Options.CurrentGameMode)
{
case CustomGameMode.HideAndSeek:
CustomHnS.PlayerRoles.Remove(id);
break;
case CustomGameMode.Mingle when data.Character.IsAlive():
Mingle.HandleDisconnect();
break;
case CustomGameMode.BedWars:
BedWars.OnDisconnect(data.Character);
break;
}
if (data.Character.Is(CustomRoles.Lovers) && data.Character.IsAlive())
{
foreach (PlayerControl lovers in Main.LoversPlayers)
{
Main.IsLoversDead = true;
Main.PlayerStates[lovers.PlayerId].RemoveSubRole(CustomRoles.Lovers);
}
Main.LoversPlayers.RemoveAll(x => x.PlayerId == id);
}
switch (data.Character.GetCustomRole())
{
case CustomRoles.Pelican:
Pelican.OnPelicanDied(id);
break;
case CustomRoles.Markseeker:
Markseeker.OnDeath(data.Character);
break;
case CustomRoles.Jackal:
Jackal.Instances.Do(x => x.PromoteSidekick());
break;
}
if (Executioner.Target.ContainsValue(id))
Executioner.ChangeRoleByTarget(data.Character);
if (Lawyer.Target.ContainsValue(id))
Lawyer.ChangeRoleByTarget(data.Character);
if (Spiritualist.SpiritualistTarget == id)
Spiritualist.RemoveTarget();
if (CopyCat.PlayerIdList.Remove(id))
CopyCat.Instances.RemoveAll(x => !x.CopyCatPC);
Postman.CheckAndResetTargets(data.Character);
GhostRolesManager.AssignedGhostRoles.Remove(id);
PlayerState state = Main.PlayerStates[id];
if (state.deathReason == PlayerState.DeathReason.etc) state.deathReason = PlayerState.DeathReason.Disconnected;
if (!state.IsDead) state.SetDead();
Utils.AfterPlayerDeathTasks(data.Character, GameStates.IsMeeting, true);
try { state.Role.Remove(id); }
catch (Exception e) { Utils.ThrowException(e); }
NameNotifyManager.Notifies.Remove(id);
data.Character.RpcSetName(data.Character.GetRealName(true));
PlayerGameOptionsSender.RemoveSender(data.Character);
}
// Additional description of the reason for disconnection
switch (reason)
{
case DisconnectReasons.Hacking:
Logger.SendInGame(string.Format(GetString("PlayerLeftByAU-Anticheat"), data?.PlayerName), Color.yellow);
break;
}
Logger.Info($"{data?.PlayerName} - (ClientID: {data?.Id} / FriendCode: {data?.FriendCode}) - Disconnected: {reason}, Ping: ({AmongUsClient.Instance.Ping})", "Session");
if (AmongUsClient.Instance.AmHost)
{
Main.SayStartTimes.Remove(__instance.ClientId);
Main.SayBanwordsTimes.Remove(__instance.ClientId);
Main.PlayerVersion.Remove(data?.Character?.PlayerId ?? byte.MaxValue);
if (data != null && data.Character)
{
uint netid = data.Character.NetId;
LateTask.New(() =>
{
if (GameStates.IsOnlineGame)
{
var message = new DespawnGameDataMessage(netid);
#if IL2CPP
AmongUsClient.Instance.LateBroadcastReliableMessage(message.CastFast<IGameDataMessage>());
#else
AmongUsClient.Instance.LateBroadcastReliableMessage(message);
#endif
}
if (GameStates.IsLobby)
Utils.DirtyName.Add(PlayerControl.LocalPlayer.PlayerId);
}, 2.5f, "Repeat Despawn", false);
}
if (GameStates.IsLobby)
Options.AutoSetFactionMinMaxSettings();
}
GameEndChecker.SetDirtyCheckEnd();
Utils.CountAlivePlayers(true);
}
catch (NullReferenceException) { }
catch (Exception ex) { Logger.Error(ex.ToString(), "OnPlayerLeftPatch.Postfix"); }
finally
{
if (!GameStates.IsLobby && GameStates.IsInTask && !ExileController.Instance)
Utils.NotifyRoles(ForceLoop: true);
}
}
}
[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.Spawn))]
internal static class InnerNetClientSpawnPatch
{
public static void Postfix([HarmonyArgument(1)] int ownerId, [HarmonyArgument(2)] SpawnFlags flags)
{
if (!AmongUsClient.Instance.AmHost || flags != SpawnFlags.IsClientCharacter) return;
ClientData client = Utils.GetClientById(ownerId);
Logger.Msg($"Spawn player data: ID {client?.Character?.PlayerId}: {client?.PlayerName}", "CreatePlayer");
Main.ForceRebuildCachesPlayerControls();
Utils.UpdatePlayerIdCache();
if (client == null || !client.Character // client is null
|| client.ColorId < 0 || Palette.PlayerColors.Length <= client.ColorId) // invalid client color
Logger.Warn("client is null or client have invalid color", "TrySyncAndSendMessage");
else
{
LateTask.New(() => OptionItem.SyncAllOptions(client.Id), 3f, "Sync All Options For New Player");
LateTask.New(() =>
{
if (Main.OverrideWelcomeMsg != "")
Utils.SendMessage(Main.OverrideWelcomeMsg, client.Character.PlayerId, importance: MessageImportance.Low);
else
TemplateManager.SendTemplate("welcome", client.Character.PlayerId, true, importance: MessageImportance.Low);
}, GameStates.CurrentServerType == GameStates.ServerType.Niko ? 7f : 3f, "Welcome Message");
if (GameStates.IsOnlineGame && !client.Character.IsHost())
{
LateTask.New(() =>
{
if (GameStates.IsLobby && client.Character && LobbyBehaviour.Instance && GameStates.CurrentServerType == GameStates.ServerType.Vanilla)
{
// Only for vanilla
if (!client.Character.IsModdedClient())
{
// This kicks the host now on vanilla regions
// MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(LobbyBehaviour.Instance.NetId, (byte)RpcCalls.LobbyTimeExpiring, SendOption.None, client.Id);
// writer.WritePacked((int)GameStartManagerPatch.Timer);
// writer.Write(false);
// AmongUsClient.Instance.FinishRpcImmediately(writer);
}
// Non-host modded client
else
{
MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, (byte)CustomRPC.SyncLobbyTimer, SendOption.Reliable, client.Id);
writer.Write(GameStartManagerPatch.TimerStartTS.ToString());
AmongUsClient.Instance.FinishRpcImmediately(writer);
}
}
}, IRandom.Instance.Next(7, 13), "Sync Lobby Timer RPC");
}
}
if (client != null && client.Character) Main.GuessNumber[client.Character.PlayerId] = [-1, 7];
if (Main.OverrideWelcomeMsg == string.Empty && Main.PlayerStates.Count > 0 && client != null && Main.ClientIdList.Contains(client.Id))
{
if (Options.AutoDisplayKillLog.GetBool())
{
LateTask.New(() =>
{
if (!AmongUsClient.Instance.IsGameStarted && client.Character)
{
Main.IsChatCommand = true;
Utils.ShowKillLog(client.Character.PlayerId);
}
}, 1f, "DisplayKillLog");
}
if (Options.AutoDisplayLastAddOns.GetBool())
{
LateTask.New(() =>
{
if (!AmongUsClient.Instance.IsGameStarted && client.Character)
{
Main.IsChatCommand = true;
Utils.ShowLastAddOns(client.Character.PlayerId);
}
}, 1.1f, "DisplayLastAddOns");
}
if (Options.AutoDisplayLastRoles.GetBool())
{
LateTask.New(() =>
{
if (!AmongUsClient.Instance.IsGameStarted && client.Character)
{
Main.IsChatCommand = true;
Utils.ShowLastRoles(client.Character.PlayerId);
}
}, 1.2f, "DisplayLastRoles");
}
if (Options.AutoDisplayLastResult.GetBool())
{
LateTask.New(() =>
{
if (!AmongUsClient.Instance.IsGameStarted && client.Character)
{
Main.IsChatCommand = true;
Utils.ShowLastResult(client.Character.PlayerId);
}
}, 1.3f, "DisplayLastResult");
}
// if (PlayerControl.LocalPlayer.FriendCode.GetDevUser().Up && Options.EnableUpMode.GetBool())
// {
// LateTask.New(() =>
// {
// if (!AmongUsClient.Instance.IsGameStarted && client.Character != null)
// {
// Main.IsChatCommand = true;
// Utils.SendMessage($"{GetString("Message.YTPlanNotice")} {PlayerControl.LocalPlayer.FriendCode.GetDevUser().UpName}", client.Character.PlayerId);
// }
// }, 1.4f, "DisplayUpWarnning");
// }
}
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CheckName))]
internal static class PlayerControlCheckNamePatch
{
public static void Postfix(PlayerControl __instance, ref string playerName)
{
if (!AmongUsClient.Instance.AmHost || !GameStates.IsLobby) return;
if (BanManager.CheckDenyNamePlayer(__instance, playerName)) return;
if (Main.AllClientRealNames.TryAdd(__instance.OwnerId, playerName)) RPC.SyncAllClientRealNames();
string name = playerName;
if (Options.FormatNameMode.GetInt() == 2 && __instance.Data.ClientId != AmongUsClient.Instance.ClientId)
name = Main.Get_TName_Snacks;
else
{
name = name.RemoveHtmlTags().Replace(@"\", string.Empty).Replace("/", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("<", string.Empty).Replace(">", string.Empty);
if (name.Length > 10) name = name[..10];
if (Options.DisableEmojiName.GetBool()) name = Regex.Replace(name, @"\p{Cs}", string.Empty);
if (Regex.Replace(Regex.Replace(name, @"\s", string.Empty), @"[\x01-\x1F,\x7F]", string.Empty).Length < 1) name = Main.Get_TName_Snacks;
}
Main.AllPlayerNames[__instance.PlayerId] = name;
Logger.Info($"PlayerId: {__instance.PlayerId} - playerName: {playerName} - name: {name}", "Name player");
RPC.SyncAllPlayerNames();
if (__instance && !name.Equals(playerName))
{
Logger.Warn($"Standard nickname: {playerName} => {name}", "Name Format");
playerName = name;
}
}
}
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetColor))]
internal static class SetColorPatch
{
public static void Postfix(PlayerControl __instance, byte bodyColor)
{
if (Main.IntroDestroyed || !__instance) return;
string colorName = Palette.GetColorName(bodyColor);
Logger.Info($"{__instance.GetRealName()}'s color is {colorName}", "RpcSetColor");
if (colorName == "???")
{
LateTask.New(() =>
{
if (Options.KickSlowJoiningPlayers.GetBool() && __instance && !Main.PlayerColors.ContainsKey(__instance.PlayerId))
{
ClientData client = __instance.GetClient();
if (client != null)
{
Logger.SendInGame(GetString("Error.InvalidColor") + $" {client.Id}/{client.PlayerName}", Color.yellow);
AmongUsClient.Instance.KickPlayer(client.Id, false);
}
}
}, 5f, "fortegreen bean color kick");
}
if (bodyColor == 255) return;
Main.PlayerColors[__instance.PlayerId] = Palette.PlayerColors[bodyColor];
}
}
// Next 2: from https://github.com/EnhancedNetwork/TownofHost-Enhanced/blob/main/Patches/LobbyPatch.cs
[HarmonyPatch(typeof(PlayerMaterial), nameof(PlayerMaterial.SetColors), typeof(int), typeof(Material))]
internal static class PlayerMaterialPatch
{
public static void Prefix([HarmonyArgument(0)] ref int colorId)
{
if (colorId < 0 || colorId >= Palette.PlayerColors.Length)
colorId = 0;
}
}
[HarmonyPatch(typeof(NetworkedPlayerInfo), nameof(NetworkedPlayerInfo.Init))]
internal static class NetworkedPlayerInfoInitPatch
{
public static void Postfix(NetworkedPlayerInfo __instance)
{
foreach (var outfit in __instance.Outfits)
{
if (outfit.Value != null)
{
if (outfit.Value.ColorId < 0 || outfit.Value.ColorId >= Palette.PlayerColors.Length)
outfit.Value.ColorId = 0;
}
}
}
}