diff --git a/EXILED/Exiled.API/Enums/AuthenticationType.cs b/EXILED/Exiled.API/Enums/AuthenticationType.cs index 9de49f902..0590ea097 100644 --- a/EXILED/Exiled.API/Enums/AuthenticationType.cs +++ b/EXILED/Exiled.API/Enums/AuthenticationType.cs @@ -42,5 +42,10 @@ public enum AuthenticationType /// Indicates that the player has been authenticated as DedicatedServer. /// DedicatedServer, + + /// + /// Indicates that the player has been authenticated during Offline mode. + /// + Offline, } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Extensions/StringExtensions.cs b/EXILED/Exiled.API/Extensions/StringExtensions.cs index 69b184bc6..37d1a6777 100644 --- a/EXILED/Exiled.API/Extensions/StringExtensions.cs +++ b/EXILED/Exiled.API/Extensions/StringExtensions.cs @@ -161,7 +161,11 @@ public static string GetBefore(this string input, char symbol) /// /// The user id. /// Returns the raw user id. - public static string GetRawUserId(this string userId) => userId.Substring(0, userId.LastIndexOf('@')); + public static string GetRawUserId(this string userId) + { + int index = userId.IndexOf('@'); + return index == -1 ? userId : userId.Substring(0, index); + } /// /// Gets a SHA256 hash of a player's user id without the authentication. diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs index b62524745..52d725bdd 100644 --- a/EXILED/Exiled.API/Features/Player.cs +++ b/EXILED/Exiled.API/Features/Player.cs @@ -282,6 +282,7 @@ public AuthenticationType AuthenticationType "northwood" => AuthenticationType.Northwood, "localhost" => AuthenticationType.LocalHost, "ID_Dedicated" => AuthenticationType.DedicatedServer, + "offline" => AuthenticationType.Offline, _ => AuthenticationType.Unknown, }; } @@ -1300,7 +1301,7 @@ public static Player Get(string args) if (int.TryParse(args, out int id)) return Get(id); - if (args.EndsWith("@steam") || args.EndsWith("@discord") || args.EndsWith("@northwood")) + if (args.EndsWith("@steam") || args.EndsWith("@discord") || args.EndsWith("@northwood") || args.EndsWith("@offline")) { foreach (Player player in Dictionary.Values) { diff --git a/EXILED/Exiled.Events/Patches/Events/Player/Verified.cs b/EXILED/Exiled.Events/Patches/Events/Player/Verified.cs index 585457a43..ce6697b5d 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/Verified.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/Verified.cs @@ -7,16 +7,20 @@ namespace Exiled.Events.Patches.Events.Player { +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter using System; + using System.Collections.Generic; + using System.Reflection.Emit; using API.Features; + using API.Features.Pools; using CentralAuth; using Exiled.API.Extensions; using Exiled.Events.EventArgs.Player; - using HarmonyLib; -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + using static HarmonyLib.AccessTools; /// /// Patches . @@ -25,12 +29,16 @@ namespace Exiled.Events.Patches.Events.Player [HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.FinalizeAuthentication))] internal static class Verified { - private static void Postfix(PlayerAuthenticationManager __instance) + /// + /// Called after the player has been verified. + /// + /// The player's hub. + internal static void PlayerVerified(ReferenceHub hub) { - if (!Player.UnverifiedPlayers.TryGetValue(__instance._hub.gameObject, out Player player)) - Joined.CallEvent(__instance._hub, out player); + if (!Player.UnverifiedPlayers.TryGetValue(hub.gameObject, out Player player)) + Joined.CallEvent(hub, out player); - Player.Dictionary.Add(__instance._hub.gameObject, player); + Player.Dictionary.Add(hub.gameObject, player); player.IsVerified = true; player.RawUserId = player.UserId.GetRawUserId(); @@ -39,5 +47,41 @@ private static void Postfix(PlayerAuthenticationManager __instance) Handlers.Player.OnVerified(new VerifiedEventArgs(player)); } + + private static void Postfix(PlayerAuthenticationManager __instance) + { + PlayerVerified(__instance._hub); + } + } + + /// + /// Patches . + /// Adds the event during offline mode. + /// + [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.UserCode_CmdSetNick__String))] + internal static class VerifiedOfflineMode + { + private static IEnumerable Transpiler(IEnumerable instructions) + { + List newInstructions = ListPool.Pool.Get(instructions); + + const int offset = 1; + int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt && x.OperandIs(Method(typeof(CharacterClassManager), nameof(CharacterClassManager.SyncServerCmdBinding)))) + offset; + + newInstructions.InsertRange( + index, + new[] + { + // Verified.PlayerVerified(this._hub); + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldfld, Field(typeof(NicknameSync), nameof(NicknameSync._hub))), + new CodeInstruction(OpCodes.Call, Method(typeof(Verified), nameof(Verified.PlayerVerified))), + }); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs b/EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs new file mode 100644 index 000000000..46c412dc3 --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs @@ -0,0 +1,164 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Generic +{ +#pragma warning disable SA1402 // File may only contain a single type + using System.Collections.Generic; + using System.Reflection.Emit; + + using API.Features.Pools; + using CentralAuth; + using HarmonyLib; + using PluginAPI.Core.Interfaces; + using PluginAPI.Events; + + using static HarmonyLib.AccessTools; + + /// + /// Patches to add an @offline suffix to UserIds in Offline Mode. + /// + [HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.Start))] + internal static class OfflineModeIds + { + private static IEnumerable Transpiler(IEnumerable instructions) + { + List newInstructions = ListPool.Pool.Get(instructions); + + const int offset = -1; + int index = newInstructions.FindLastIndex(instruction => instruction.opcode == OpCodes.Call && instruction.OperandIs(PropertySetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId)))) + offset; + + newInstructions.InsertRange( + index, + new[] + { + new CodeInstruction(OpCodes.Call, Method(typeof(OfflineModeIds), nameof(BuildUserId))), + }); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + + private static string BuildUserId(string userId) => $"{userId}@offline"; + } + + /// + /// Patches to add the player's UserId to the dictionary. + /// + [HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.Start))] + internal static class OfflineModePlayerIds + { + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + Label skipLabel = generator.DefineLabel(); + + const int offset = 1; + int index = newInstructions.FindLastIndex(instruction => instruction.opcode == OpCodes.Call && instruction.OperandIs(PropertySetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId)))) + offset; + + // if (!Player.PlayersUserIds.ContainsKey(this.UserId)) + // Player.PlayersUserIds.Add(this.UserId, this._hub); + newInstructions.InsertRange( + index, + new[] + { + // if (Player.PlayersUserIds.ContainsKey(this.UserId)) goto skip; + new(OpCodes.Ldsfld, Field(typeof(PluginAPI.Core.Player), nameof(PluginAPI.Core.Player.PlayersUserIds))), + new(OpCodes.Ldarg_0), + new(OpCodes.Call, PropertyGetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId))), + new(OpCodes.Callvirt, Method(typeof(Dictionary), nameof(Dictionary.ContainsKey))), + new(OpCodes.Brtrue_S, skipLabel), + + // Player.PlayersUserIds.Add(this.UserId, this._hub); + new(OpCodes.Ldsfld, Field(typeof(PluginAPI.Core.Player), nameof(PluginAPI.Core.Player.PlayersUserIds))), + new(OpCodes.Ldarg_0), + new(OpCodes.Call, PropertyGetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId))), + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager._hub))), + new(OpCodes.Callvirt, Method(typeof(Dictionary), nameof(Dictionary.Add))), + + // skip: + new CodeInstruction(OpCodes.Nop).WithLabels(skipLabel), + }); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + } + + /// + /// Patches to prevent it from executing the event when the server is in offline mode. + /// + [HarmonyPatch(typeof(ReferenceHub), nameof(ReferenceHub.Start))] + internal static class OfflineModeReferenceHub + { + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + const int offset = 1; + int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt) + offset; + + Label returnLabel = generator.DefineLabel(); + + newInstructions.InsertRange( + index, + new[] + { + new CodeInstruction(OpCodes.Br_S, returnLabel), + }); + + newInstructions[newInstructions.Count - 1].WithLabels(returnLabel); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + } + + /// + /// Patches to execute the event when the server is in offline mode. + /// + [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.UserCode_CmdSetNick__String))] + internal static class OfflineModeJoin + { + private static IEnumerable Transpiler(IEnumerable instructions) + { + List newInstructions = ListPool.Pool.Get(instructions); + + const int offset = 1; + int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt && x.OperandIs(Method(typeof(CharacterClassManager), nameof(CharacterClassManager.SyncServerCmdBinding)))) + offset; + + // EventManager.ExecuteEvent(new PlayerJoinedEvent(this._hub)); + newInstructions.InsertRange( + index, + new[] + { + // EventManager.ExecuteEvent(new PlayerJoinedEvent(this._hub)); + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldfld, Field(typeof(NicknameSync), nameof(NicknameSync._hub))), + new CodeInstruction(OpCodes.Call, Method(typeof(OfflineModeJoin), nameof(ExecuteNwEvent))), + }); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + + private static void ExecuteNwEvent(ReferenceHub hub) + { + EventManager.ExecuteEvent(new PlayerJoinedEvent(hub)); + } + } +} \ No newline at end of file