Skip to content

Commit

Permalink
Offline mode support (#19)
Browse files Browse the repository at this point in the history
* Fix Offline-mode breaking everything

* Add `offline` authentication type and append `@offline` to UserIds during offline mode

* Add offline id support to Player.Get

* Comment transpilers

---------

Co-authored-by: Yamato <66829532+louis1706@users.noreply.github.com>
  • Loading branch information
x3rt and louis1706 authored Aug 6, 2024
1 parent eb046ac commit a7354b7
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 8 deletions.
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 @@ -292,6 +292,7 @@ public AuthenticationType AuthenticationType
"northwood" => AuthenticationType.Northwood,
"localhost" => AuthenticationType.LocalHost,
"ID_Dedicated" => AuthenticationType.DedicatedServer,
"offline" => AuthenticationType.Offline,
_ => AuthenticationType.Unknown,
};
}
Expand Down Expand Up @@ -1310,7 +1311,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;

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<CodeInstruction>.Pool.Return(newInstructions);
}
}
}
164 changes: 164 additions & 0 deletions EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// -----------------------------------------------------------------------
// <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[]
{
// 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<string, IGameComponent>), nameof(Dictionary<string, IGameComponent>.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<string, IGameComponent>), nameof(Dictionary<string, IGameComponent>.Add))),

// skip:
new CodeInstruction(OpCodes.Nop).WithLabels(skipLabel),
});

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[]
{
// 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<CodeInstruction>.Pool.Return(newInstructions);
}

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

0 comments on commit a7354b7

Please sign in to comment.