Skip to content
62 changes: 62 additions & 0 deletions EXILED/Exiled.Events/EventArgs/Server/RoundStartingEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// -----------------------------------------------------------------------
// <copyright file="RoundStartingEventArgs.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.Events.EventArgs.Server
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Exiled.Events.EventArgs.Interfaces;

/// <summary>
/// Contains all information before the start of a round.
/// </summary>
public class RoundStartingEventArgs : IDeniableEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="RoundStartingEventArgs" /> class.
/// </summary>
/// <param name="timeLeft"><inheritdoc cref="TimeLeft"/></param>
/// <param name="playerCount"><inheritdoc cref="PlayerCount"/></param>
/// <param name="topPlayer"><inheritdoc cref="TopPlayer"/></param>
/// <param name="originalTimeLeft"><inheritdoc cref="OriginalTimeLeft"/></param>
public RoundStartingEventArgs(short timeLeft, short originalTimeLeft, int topPlayer, int playerCount)
{
TimeLeft = timeLeft;
OriginalTimeLeft = originalTimeLeft;
TopPlayer = topPlayer;
PlayerCount = playerCount;
IsAllowed = TimeLeft == -1;
}

/// <summary>
/// Gets or sets the time before the start of the Round.
/// </summary>
public int TimeLeft { get; set; }

/// <summary>
/// Gets or sets the time before the start of the Round.
/// </summary>
public int OriginalTimeLeft { get; set; }

/// <summary>
/// Gets or sets the maximum number of Player on the server since restart.
/// </summary>
public int TopPlayer { get; set; }

/// <summary>
/// Gets the number of Player.
/// </summary>
public int PlayerCount { get; }

/// <inheritdoc/>
public bool IsAllowed { get; set; }
}
}
11 changes: 11 additions & 0 deletions EXILED/Exiled.Events/Handlers/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public static class Server
/// </summary>
public static Event RoundStarted { get; set; } = new();

/// <summary>
/// Invoked after the start of a new round.
/// </summary>
public static Event<RoundStartingEventArgs> RoundStarting { get; set; } = new();

/// <summary>
/// Invoked after all players have spawned at the start of a new round.
/// </summary>
Expand Down Expand Up @@ -137,6 +142,12 @@ public static class Server
/// </summary>
public static void OnWaitingForPlayers() => WaitingForPlayers.InvokeSafely();

/// <summary>
/// Called before the start of a new round.
/// </summary>
/// <param name="ev">The <see cref="RoundStartingEventArgs"/> instance.</param>
public static void OnRoundStarting(RoundStartingEventArgs ev) => RoundStarting.InvokeSafely(ev);

/// <summary>
/// Called after the start of a new round.
/// </summary>
Expand Down
118 changes: 118 additions & 0 deletions EXILED/Exiled.Events/Patches/Events/Server/RoundStarting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// -----------------------------------------------------------------------
// <copyright file="RoundStarting.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.Events.Patches.Events.Server
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

using Exiled.API.Features;
using Exiled.API.Features.Pools;
using Exiled.Events.EventArgs.Server;
using HarmonyLib;

using static HarmonyLib.AccessTools;

/// <summary>
/// Patches <see cref="CharacterClassManager.Init()" />.
/// Adds the <see cref="Handlers.Server.RoundStarting" /> event.
/// </summary>
[HarmonyPatch]
internal class RoundStarting
{
#pragma warning disable SA1600 // Elements should be documented
public static Type PrivateType { get; internal set; }

private static MethodInfo TargetMethod()
{
PrivateType = typeof(CharacterClassManager).GetNestedTypes(all)
.FirstOrDefault(currentType => currentType.Name.Contains("Init"));
if (PrivateType == null)
throw new Exception("State machine type for Init not found.");
MethodInfo moveNextMethod = PrivateType.GetMethod("MoveNext", all);

if (moveNextMethod == null)
throw new Exception("MoveNext method not found in the state machine type.");
return moveNextMethod;
}

private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

const string TimeLeft = "<timeLeft>5__3";
const string OriginalTimeLeft = "<originalTimeLeft>5__2";
const string TopPlayer = "<topPlayers>5__4";

LocalBuilder ev = generator.DeclareLocal(typeof(RoundStartingEventArgs));
int offset = -4;
int index = newInstructions.FindLastIndex(x => x.Calls(Method(typeof(CharacterClassManager), nameof(CharacterClassManager.ForceRoundStart)))) + offset;

List<Label> labels = newInstructions[index].ExtractLabels();
Label skip = (Label)newInstructions[index + 3].operand;
newInstructions.RemoveRange(index, 4);

newInstructions.InsertRange(index, new[]
{
// this.TimeLeft
new CodeInstruction(OpCodes.Ldarg_0).WithLabels(labels),
new(OpCodes.Ldfld, Field(PrivateType, TimeLeft)),

// this.OriginalTimeLeft
new(OpCodes.Ldarg_0),
new(OpCodes.Ldfld, Field(PrivateType, OriginalTimeLeft)),

// this.TopPlayer
new(OpCodes.Ldarg_0),
new(OpCodes.Ldfld, Field(PrivateType, TopPlayer)),

// playerCount
new(OpCodes.Ldloc_2),

// RoundStartingEventArgs ev = new(short, short, int, int)
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(RoundStartingEventArgs))[0]),
new(OpCodes.Dup),
new(OpCodes.Stloc_S, ev.LocalIndex),

// Handlers.Server.OnRoundStarting(ev)
new(OpCodes.Call, Method(typeof(Handlers.Server), nameof(Handlers.Server.OnRoundStarting))),

// this.TimeLeft = ev.TimeLeft
new(OpCodes.Ldarg_0),
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(RoundStartingEventArgs), nameof(RoundStartingEventArgs.TimeLeft))),
new(OpCodes.Stfld, Field(PrivateType, TimeLeft)),

// this.OriginalTimeLeft = ev.OriginalTimeLeft
new(OpCodes.Ldarg_0),
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(RoundStartingEventArgs), nameof(RoundStartingEventArgs.OriginalTimeLeft))),
new(OpCodes.Stfld, Field(PrivateType, OriginalTimeLeft)),

// this.TopPlayer = ev.TopPlayer
new(OpCodes.Ldarg_0),
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(RoundStartingEventArgs), nameof(RoundStartingEventArgs.TopPlayer))),
new(OpCodes.Stfld, Field(PrivateType, TopPlayer)),

// if (!ev.IsAllowed)
// skip;
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(RoundStartingEventArgs), nameof(RoundStartingEventArgs.IsAllowed))),
new(OpCodes.Brfalse_S, skip),
});

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

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