Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline mode support #19

Merged
merged 6 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions EXILED/Exiled.API/Enums/AuthenticationType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@ public enum AuthenticationType
/// Indicates that the player has been authenticated as DedicatedServer.
/// </summary>
DedicatedServer,

/// <summary>
/// Indicates that the player has been authenticated during Offline mode.
/// </summary>
Offline,
}
}
6 changes: 5 additions & 1 deletion EXILED/Exiled.API/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,11 @@ public static string GetBefore(this string input, char symbol)
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>Returns the raw user id.</returns>
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);
}

/// <summary>
/// Gets a SHA256 hash of a player's user id without the authentication.
Expand Down
3 changes: 2 additions & 1 deletion EXILED/Exiled.API/Features/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ public AuthenticationType AuthenticationType
"northwood" => AuthenticationType.Northwood,
"localhost" => AuthenticationType.LocalHost,
"ID_Dedicated" => AuthenticationType.DedicatedServer,
"offline" => AuthenticationType.Offline,
_ => AuthenticationType.Unknown,
};
}
Expand Down Expand Up @@ -1295,7 +1296,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)
{
Expand Down
56 changes: 50 additions & 6 deletions EXILED/Exiled.Events/Patches/Events/Player/Verified.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Patches <see cref="PlayerAuthenticationManager.FinalizeAuthentication" />.
Expand All @@ -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)
/// <summary>
/// Called after the player has been verified.
/// </summary>
/// <param name="hub">The player's hub.</param>
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();
Expand All @@ -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);
}
}

/// <summary>
/// Patches <see cref="NicknameSync.UserCode_CmdSetNick__String" />.
/// Adds the <see cref="Handlers.Player.Verified" /> event during offline mode.
/// </summary>
[HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.UserCode_CmdSetNick__String))]
internal static class VerifiedOfflineMode
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.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;

// Verified.PlayerVerified(this._hub);
newInstructions.InsertRange(
index,
new[]
{
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))),
x3rt marked this conversation as resolved.
Show resolved Hide resolved
});

for (int i = 0; i < newInstructions.Count; i++)
yield return newInstructions[i];

ListPool<CodeInstruction>.Pool.Return(newInstructions);
}
}
}
158 changes: 158 additions & 0 deletions EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// -----------------------------------------------------------------------
// <copyright file="OfflineModeIds.cs" company="Exiled Team">
// Copyright (c) Exiled Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

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;

/// <summary>
/// Patches <see cref="PlayerAuthenticationManager.Start"/> to add an @offline suffix to UserIds in Offline Mode.
/// </summary>
[HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.Start))]
internal static class OfflineModeIds
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.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<CodeInstruction>.Pool.Return(newInstructions);
}

private static string BuildUserId(string userId) => $"{userId}@offline";
}

/// <summary>
/// Patches <see cref="PlayerAuthenticationManager.Start"/> to add the player's UserId to the <see cref="PluginAPI.Core.Player.PlayersUserIds"/> dictionary.
/// </summary>
[HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.Start))]
internal static class OfflineModePlayerIds
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.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[]
{
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<string, IGameComponent>), nameof(Dictionary<string, IGameComponent>.ContainsKey))),
new(OpCodes.Brtrue_S, skipLabel),
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<string, IGameComponent>), nameof(Dictionary<string, IGameComponent>.Add))),
new CodeInstruction(OpCodes.Nop).WithLabels(skipLabel),
x3rt marked this conversation as resolved.
Show resolved Hide resolved
});

for (int i = 0; i < newInstructions.Count; i++)
yield return newInstructions[i];

ListPool<CodeInstruction>.Pool.Return(newInstructions);
}
}

/// <summary>
/// Patches <see cref="ReferenceHub.Start"/> to prevent it from executing the <see cref="PluginAPI.Events.PlayerLeftEvent"/> event when the server is in offline mode.
/// </summary>
[HarmonyPatch(typeof(ReferenceHub), nameof(ReferenceHub.Start))]
internal static class OfflineModeReferenceHub
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.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<CodeInstruction>.Pool.Return(newInstructions);
}
}

/// <summary>
/// Patches <see cref="NicknameSync.UserCode_CmdSetNick__String"/> to execute the <see cref="PlayerJoinedEvent"/> event when the server is in offline mode.
/// </summary>
[HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.UserCode_CmdSetNick__String))]
internal static class OfflineModeJoin
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.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[]
{
new CodeInstruction(OpCodes.Ldarg_0),
new CodeInstruction(OpCodes.Ldfld, Field(typeof(NicknameSync), nameof(NicknameSync._hub))),
new CodeInstruction(OpCodes.Call, Method(typeof(OfflineModeJoin), nameof(ExecuteNwEvent))),
x3rt marked this conversation as resolved.
Show resolved Hide resolved
});

for (int i = 0; i < newInstructions.Count; i++)
yield return newInstructions[i];

ListPool<CodeInstruction>.Pool.Return(newInstructions);
}

private static void ExecuteNwEvent(ReferenceHub hub)
{
EventManager.ExecuteEvent(new PlayerJoinedEvent(hub));
}
}
}
Loading