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