BREADKING CHANGE feat: Add rich and low grav rounds

This commit is contained in:
MSWS
2025-11-10 03:31:03 -08:00
parent 60de8b54db
commit 722c29bde7
29 changed files with 295 additions and 65 deletions

View File

@@ -21,4 +21,55 @@ public interface IPlayerFinder {
var matches = GetOnline().Where(p => p.Name.Contains(name)).ToList();
return matches.Count == 1 ? matches[0] : null;
}
List<IOnlinePlayer> GetMulti(string query, out string name,
IOnlinePlayer? executor = null) {
var result = query switch {
"@all" => GetOnline().ToList(),
"@me" => executor != null ? new List<IOnlinePlayer> { executor } : [],
"@!me" => executor != null ?
GetOnline().Where(p => p.Id != executor.Id).ToList() :
GetOnline().ToList(),
_ => GetSingle(query) != null ?
new List<IOnlinePlayer> { GetSingle(query)! } : []
};
name = "no players found";
name = query switch {
"@all" => "all players",
"@me" => executor != null ? executor.Name : "no one",
"@!me" => executor != null ?
$"all players except {executor.Name}" :
"all players",
_ => GetSingle(query) != null ?
GetSingle(query)!.Name :
"no players found"
};
return result;
}
IOnlinePlayer? GetSingle(string query) {
if (query.StartsWith("#")) {
var id = query[1..];
var byId = GetPlayerById(id);
if (byId != null) return byId;
var byName = GetOnline().FirstOrDefault(p => p.Name == id);
return byName;
}
var byNameExact = GetOnline().FirstOrDefault(p => p.Name == query);
if (byNameExact != null) return byNameExact;
var contains = GetOnline().Where(p => p.Name.Contains(query)).ToList();
if (contains.Count == 1) return contains[0];
contains = GetOnline()
.Where(p
=> p.Name.Contains(query, StringComparison.InvariantCultureIgnoreCase))
.ToList();
return contains.Count == 1 ? contains[0] : null;
}
}

View File

@@ -33,21 +33,14 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
return Task.FromResult(CommandResult.ERROR);
}
var target = executor;
List<IOnlinePlayer> targets = [executor];
Server.NextWorldUpdateAsync(() => {
if (info.ArgCount == 3) {
var result = finder.GetPlayerByName(info.Args[2]);
if (result == null) {
info.ReplySync($"Player '{info.Args[2]}' not found.");
return;
}
var name = executor.Name;
if (info.ArgCount == 3) targets = finder.GetMulti(info.Args[2], out name);
foreach (var player in targets) shop.GiveItem(player, item);
target = result;
}
shop.GiveItem(target, item);
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");
info.ReplySync($"Gave item '{item.Name}' to {name}.");
});
return Task.FromResult(CommandResult.SUCCESS);
}

View File

@@ -15,6 +15,9 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
public void Dispose() { }
public string Id => "setrole";
@@ -24,7 +27,10 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
IRole roleToAssign = new TraitorRole(provider);
// IOnlinePlayer targetPlayer = executor;
List<IOnlinePlayer> targets = [executor];
var targetName = executor.Name;
IRole roleToAssign = new TraitorRole(provider);
if (info.ArgCount == 2)
switch (info.Args[1].ToLowerInvariant()) {
case "d" or "det" or "detective" or "ct":
@@ -33,18 +39,30 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
case "i" or "inn" or "innocent":
roleToAssign = new InnocentRole(provider);
break;
default:
targets = finder.GetMulti(info.Args[1], out targetName, executor);
break;
}
if (info.ArgCount == 3) {
targets = finder.GetMulti(info.Args[2], out targetName, executor);
}
Server.NextWorldUpdate(() => {
var ev = new PlayerRoleAssignEvent(executor, roleToAssign);
bus.Dispatch(ev);
if (ev.IsCanceled) {
info.ReplySync("Role assignment was canceled.");
return;
foreach (var player in targets) {
var ev = new PlayerRoleAssignEvent(player, roleToAssign);
bus.Dispatch(ev);
if (ev.IsCanceled) {
info.ReplySync("Role assignment was canceled.");
return;
}
assigner.Write(player, [ev.Role]);
ev.Role.OnAssign(player);
}
assigner.Write(executor, [ev.Role]);
ev.Role.OnAssign(executor);
info.ReplySync(
"Assigned " + roleToAssign.Name + " to " + targetName + ".");
});
return Task.FromResult(CommandResult.SUCCESS);
}

View File

@@ -41,7 +41,7 @@ public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
}
Server.NextWorldUpdate(() => {
tracker.TryStartSpecialRound(round);
tracker.TryStartSpecialRound([round]);
info.ReplySync($"Started special round '{roundName}'.");
});
return Task.FromResult(CommandResult.SUCCESS);

View File

@@ -44,7 +44,7 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
// Karma deltas
public static readonly FakeConVar<int> CV_INNO_ON_TRAITOR = new(
"css_ttt_karma_inno_on_traitor",
"Karma gained when Innocent kills a Traitor", 4, ConVarFlags.FCVAR_NONE,
"Karma gained when Innocent kills a Traitor", 2, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
@@ -55,21 +55,21 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_INNO_ON_INNO_VICTIM = new(
"css_ttt_karma_inno_on_inno_victim",
"Karma gained or lost when Innocent kills another Innocent who was a victim",
-1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
-2, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_INNO_ON_INNO = new(
"css_ttt_karma_inno_on_inno",
"Karma lost when Innocent kills another Innocent", -5,
"Karma lost when Innocent kills another Innocent", -8,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_TRAITOR_ON_TRAITOR = new(
"css_ttt_karma_traitor_on_traitor",
"Karma lost when Traitor kills another Traitor", -6, ConVarFlags.FCVAR_NONE,
"Karma lost when Traitor kills another Traitor", -12, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_INNO_ON_DETECTIVE = new(
"css_ttt_karma_inno_on_detective",
"Karma lost when Innocent kills a Detective", -8, ConVarFlags.FCVAR_NONE,
"Karma lost when Innocent kills a Detective", -15, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(-50, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using TTT.API.Player;
namespace TTT.CS2.Player;

View File

@@ -38,6 +38,7 @@ public class C4ShopItem(IServiceProvider provider)
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
c4sBought++;
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
}
@@ -47,10 +48,8 @@ public class C4ShopItem(IServiceProvider provider)
return PurchaseResult.ITEM_NOT_PURCHASABLE;
if (config.MaxC4AtOnce > 0) {
var count = 0;
if (finder.GetOnline()
.Where(p => Shop.HasItem<C4ShopItem>(p))
.Any(_ => count++ >= config.MaxC4AtOnce))
if (finder.GetOnline().Count(p => Shop.HasItem<C4ShopItem>(p))
> config.MaxC4AtOnce)
return PurchaseResult.ITEM_NOT_PURCHASABLE;
}

View File

@@ -10,6 +10,7 @@ using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Locale;
namespace TTT.Shop;
@@ -30,6 +31,9 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly IMsgLocalizer localizer =
provider.GetRequiredService<IMsgLocalizer>();
private IDisposable? rewardTimer, updateTimer;
private ShopConfig config
@@ -67,7 +71,8 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
var position = count == 1 ? 1f : (float)(count - i - 1) / (count - 1);
var rewardAmount = scaleRewardAmount(position, config.MinRewardAmount,
config.MaxRewardAmount);
shop.AddBalance(player, rewardAmount, "Exploration");
shop.AddBalance(player, rewardAmount,
localizer[ShopMsgs.SHOP_EXPLORATION]);
}
});
}

View File

@@ -11,6 +11,8 @@ public static class ShopMsgs {
public static IMsg SHOP_INACTIVE => MsgFactory.Create(nameof(SHOP_INACTIVE));
public static IMsg CREDITS_NAME => MsgFactory.Create(nameof(CREDITS_NAME));
public static IMsg SHOP_EXPLORATION => MsgFactory.Create(nameof(SHOP_EXPLORATION));
public static IMsg SHOP_CANNOT_PURCHASE
=> MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE));

View File

@@ -2,6 +2,8 @@ SHOP_PREFIX: "{green}SHOP {grey}| "
SHOP_INACTIVE: "%SHOP_PREFIX%The shop is currently closed."
SHOP_ITEM_NOT_FOUND: "%SHOP_PREFIX%Could not find an item named \"{default}{0}{grey}\"."
SHOP_EXPLORATION: "Exploration"
SHOP_ITEM_DEAGLE: "One-Hit Revolver"
SHOP_ITEM_DEAGLE_DESC: "If you hit an enemy, they will die instantly. Hitting a teammate will kill you instead"
SHOP_ITEM_DEAGLE_HIT_FF: "%PREFIX%You hit a teammate!"

View File

@@ -0,0 +1,11 @@
using SpecialRoundAPI;
namespace SpecialRound.Events;
/// <summary>
/// Called when a special round is enabled.
/// Note that multiple special rounds may be enabled per round.
/// </summary>
/// <param name="round"></param>
public class SpecialRoundEnableEvent(AbstractSpecialRound round)
: SpecialRoundEvent(round);

View File

@@ -1,6 +0,0 @@
using SpecialRoundAPI;
namespace SpecialRound.Events;
public class SpecialRoundStartEvent(AbstractSpecialRound round)
: SpecialRoundEvent(round) { }

View File

@@ -0,0 +1,44 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Cvars;
using Microsoft.Extensions.DependencyInjection;
using SpecialRound.lang;
using SpecialRoundAPI;
using SpecialRoundAPI.Configs;
using TTT.API.Game;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Locale;
namespace SpecialRound.Rounds;
public class LowGravRound(IServiceProvider provider)
: AbstractSpecialRound(provider) {
public override string Name => "Low Grav";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_LOWGRAV;
public override SpecialRoundConfig Config => config;
private LowGravRoundConfig config
=> Provider.GetService<IStorage<LowGravRoundConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new LowGravRoundConfig();
private int originalGravity = 800;
public override void ApplyRoundEffects() {
var cvar = ConVar.Find("sv_gravity");
if (cvar == null) return;
originalGravity = cvar.GetPrimitiveValue<int>();
var newGravity = (int)(originalGravity * config.GravityMultiplier);
Server.NextWorldUpdate(()
=> Server.ExecuteCommand($"sv_gravity {newGravity}"));
}
public override void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
Server.NextWorldUpdate(()
=> Server.ExecuteCommand($"sv_gravity {originalGravity}"));
}
}

View File

@@ -0,0 +1,52 @@
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Events;
using SpecialRound.lang;
using SpecialRoundAPI;
using SpecialRoundAPI.Configs;
using TTT.API.Events;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Locale;
namespace SpecialRound.Rounds;
public class RichRound(IServiceProvider provider)
: AbstractSpecialRound(provider) {
public override string Name => "Rich";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_RICH;
public override SpecialRoundConfig Config => config;
private RichRoundConfig config
=> Provider.GetService<IStorage<RichRoundConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new RichRoundConfig();
public override void ApplyRoundEffects() { }
[UsedImplicitly]
[EventHandler]
public void OnBalanceChange(PlayerBalanceEvent ev) {
if (!Tracker.ActiveRounds.Contains(this)) return;
if (ev.Reason == "Round Start") {
var newBal = (int)(ev.NewBalance * config.BonusCreditsMultiplier);
ev.NewBalance = newBal;
return;
}
if (ev.NewBalance <= ev.OldBalance) return;
var gain = ev.NewBalance - ev.OldBalance;
gain = (int)(gain * config.AdditiveCreditsMultiplier);
ev.NewBalance = ev.OldBalance + gain;
}
public override bool ConflictsWith(AbstractSpecialRound other) {
return other is VanillaRound;
}
public override void OnGameState(GameStateUpdateEvent ev) { }
}

View File

@@ -82,7 +82,7 @@ public class SpeedRound(IServiceProvider provider)
public void OnDeath(PlayerDeathEvent ev) {
var game = games.ActiveGame;
if (game == null) return;
if (Tracker.CurrentRound != this) return;
if (Tracker.ActiveRounds.Contains(this)) return;
var victimRoles = roles.GetRoles(ev.Victim);
if (!victimRoles.Any(r => r is InnocentRole)) return;

View File

@@ -35,10 +35,14 @@ public class VanillaRound(IServiceProvider provider)
public override void OnGameState(GameStateUpdateEvent ev) { }
public override bool ConflictsWith(AbstractSpecialRound other) {
return other is RichRound;
}
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void OnPurchase(PlayerPurchaseItemEvent ev) {
if (Tracker.CurrentRound != this) return;
if (Tracker.ActiveRounds.Contains(this)) return;
ev.IsCanceled = true;
messenger.Message(ev.Player, locale[RoundMsgs.VANILLA_ROUND_REMINDER]);

View File

@@ -11,11 +11,13 @@ public static class SpecialRoundCollection {
services.AddModBehavior<ISpecialRoundTracker, SpecialRoundTracker>();
services.AddModBehavior<SpecialRoundSoundNotifier>();
services.AddModBehavior<SpeedRound>();
services.AddModBehavior<BhopRound>();
services.AddModBehavior<VanillaRound>();
services.AddModBehavior<SuppressedRound>();
services.AddModBehavior<SilentRound>();
services.AddModBehavior<LowGravRound>();
services.AddModBehavior<PistolRound>();
services.AddModBehavior<RichRound>();
services.AddModBehavior<SilentRound>();
services.AddModBehavior<SpeedRound>();
services.AddModBehavior<SuppressedRound>();
services.AddModBehavior<VanillaRound>();
}
}

View File

@@ -10,7 +10,7 @@ public class SpecialRoundSoundNotifier(IServiceProvider provider)
: BaseListener(provider) {
[UsedImplicitly]
[EventHandler]
public void OnSpecialRoundStart(SpecialRoundStartEvent ev) {
public void OnSpecialRoundStart(SpecialRoundEnableEvent ev) {
foreach (var player in Utilities.GetPlayers())
player.EmitSound("UI.XP.Star.Spend", null, 0.2f);
}

View File

@@ -30,20 +30,21 @@ public class SpecialRoundStarter(IServiceProvider provider)
plugin?.RegisterListener<Listeners.OnMapStart>(onMapChange);
}
public AbstractSpecialRound?
TryStartSpecialRound(AbstractSpecialRound? round) {
round ??= getSpecialRound();
public List<AbstractSpecialRound> TryStartSpecialRound(
List<AbstractSpecialRound>? rounds = null) {
rounds ??= getSpecialRounds();
var roundStart = new SpecialRoundStartEvent(round);
Bus.Dispatch(roundStart);
Messenger.MessageAll(Locale[RoundMsgs.SPECIAL_ROUND_STARTED(rounds)]);
Messenger.MessageAll(Locale[RoundMsgs.SPECIAL_ROUND_STARTED(round)]);
Messenger.MessageAll(Locale[round.Description]);
foreach (var round in rounds) {
var roundStart = new SpecialRoundEnableEvent(round);
Bus.Dispatch(roundStart);
Messenger.MessageAll(Locale[round.Description]);
round.ApplyRoundEffects();
}
round.ApplyRoundEffects();
tracker.CurrentRound = round;
tracker.RoundsSinceLastSpecial = 0;
return round;
return rounds;
}
private void onMapChange(string mapName) { roundsSinceMapChange = 0; }
@@ -62,16 +63,32 @@ public class SpecialRoundStarter(IServiceProvider provider)
if (roundsSinceMapChange < config.MinRoundsAfterMapChange) return;
if (Random.Shared.NextSingle() > config.SpecialRoundChance) return;
var specialRound = getSpecialRound();
var specialRound = getSpecialRounds();
TryStartSpecialRound(specialRound);
}
private AbstractSpecialRound getSpecialRound() {
private List<AbstractSpecialRound> getSpecialRounds() {
var selectedRounds = new List<AbstractSpecialRound>();
do {
var round = pickWeightedRound(selectedRounds);
if (round == null) break;
selectedRounds.Add(round);
} while (config.MultiRoundChance > Random.Shared.NextSingle());
return selectedRounds;
}
private AbstractSpecialRound? pickWeightedRound(
List<AbstractSpecialRound> exclude) {
var rounds = Provider.GetServices<ITerrorModule>()
.OfType<AbstractSpecialRound>()
.Where(r => r.Config.Weight > 0)
.Where(r => r.Config.Weight > 0 && !exclude.Contains(r))
.Where(r
=> !exclude.Any(er => er.ConflictsWith(r) && !r.ConflictsWith(er)))
.ToList();
if (rounds.Count == 0) return null;
var totalWeight = rounds.Sum(r => r.Config.Weight);
var roll = Random.Shared.NextDouble() * totalWeight;
foreach (var round in rounds) {
@@ -79,7 +96,6 @@ public class SpecialRoundStarter(IServiceProvider provider)
if (roll <= 0) return round;
}
throw new InvalidOperationException(
"Failed to select a special round. This should never happen.");
return null;
}
}

View File

@@ -10,6 +10,7 @@ namespace SpecialRound;
public class SpecialRoundTracker : ISpecialRoundTracker, ITerrorModule,
IListener {
public AbstractSpecialRound? CurrentRound { get; set; }
public List<AbstractSpecialRound> ActiveRounds { get; } = new();
public int RoundsSinceLastSpecial { get; set; }
public void Dispose() { }
public void Start() { }
@@ -19,5 +20,6 @@ public class SpecialRoundTracker : ISpecialRoundTracker, ITerrorModule,
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
CurrentRound = null;
ActiveRounds.Clear();
}
}

View File

@@ -5,4 +5,11 @@ public record SpecialRoundsConfig {
public int MinPlayersForSpecial { get; init; } = 5;
public int MinRoundsAfterMapChange { get; init; } = 2;
public float SpecialRoundChance { get; init; } = 0.2f;
/// <summary>
/// If a special round is started, the chance that another special round
/// will start in conjunction with it. This check is run until it fails,
/// or we run out of special rounds to start.
/// </summary>
public float MultiRoundChance { get; init; } = 0.33f;
}

View File

@@ -25,7 +25,16 @@ public class RoundMsgs {
public static IMsg SPECIAL_ROUND_PISTOL
=> MsgFactory.Create(nameof(SPECIAL_ROUND_PISTOL));
public static IMsg SPECIAL_ROUND_STARTED(AbstractSpecialRound round) {
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), round.Name);
public static IMsg SPECIAL_ROUND_RICH
=> MsgFactory.Create(nameof(SPECIAL_ROUND_RICH));
public static IMsg SPECIAL_ROUND_LOWGRAV
=> MsgFactory.Create(nameof(SPECIAL_ROUND_LOWGRAV));
public static IMsg SPECIAL_ROUND_STARTED(List<AbstractSpecialRound> round) {
var roundNames = round.Count == 1 ?
round[0].Name :
string.Join(", ", round.Select(r => r.Name));
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), roundNames);
}
}

View File

@@ -5,4 +5,6 @@ SPECIAL_ROUND_VANILLA: " {green}VANILLA{grey}: The shop has been disabled!"
SPECIAL_ROUND_SUPPRESSED: " {grey}SUPPRESSED{grey}: All pistols are silent!"
SPECIAL_ROUND_SILENT: " {grey}SILENT{grey}: All players are muted!"
SPECIAL_ROUND_PISTOL: " {blue}PISTOL{grey}: You can only use pistols this round!"
SPECIAL_ROUND_RICH: " {gold}RICH{grey}: All players start with extra credits!"
SPECIAL_ROUND_LOWGRAV: " {lightblue}LOW GRAVITY{grey}: Players can jump higher and fall slower!"
VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled."

View File

@@ -19,6 +19,8 @@ public abstract class AbstractSpecialRound(IServiceProvider provider)
public abstract void ApplyRoundEffects();
public virtual bool ConflictsWith(AbstractSpecialRound _) { return false; }
[UsedImplicitly]
[EventHandler]
public abstract void OnGameState(GameStateUpdateEvent ev);

View File

@@ -0,0 +1,6 @@
namespace SpecialRoundAPI.Configs;
public record LowGravRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.6f;
public float GravityMultiplier { get; init; } = 0.5f;
}

View File

@@ -0,0 +1,7 @@
namespace SpecialRoundAPI.Configs;
public record RichRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.75f;
public float BonusCreditsMultiplier { get; init; } = 2.0f;
public float AdditiveCreditsMultiplier { get; init; } = 3.0f;
}

View File

@@ -8,6 +8,6 @@ public interface ISpecialRoundStarter {
/// </summary>
/// <param name="round"></param>
/// <returns></returns>
public AbstractSpecialRound?
TryStartSpecialRound(AbstractSpecialRound? round);
public List<AbstractSpecialRound>? TryStartSpecialRound(
List<AbstractSpecialRound>? round = null);
}

View File

@@ -1,6 +1,7 @@
namespace SpecialRoundAPI;
public interface ISpecialRoundTracker {
public AbstractSpecialRound? CurrentRound { get; set; }
public List<AbstractSpecialRound> ActiveRounds { get; }
public int RoundsSinceLastSpecial { get; set; }
}

View File

@@ -23,7 +23,7 @@ public class SpecialRoundListener(IServiceProvider provider)
[UsedImplicitly]
[EventHandler]
public void OnRoundStart(SpecialRoundStartEvent ev) { round = ev.Round; }
public void OnRoundStart(SpecialRoundEnableEvent ev) { round = ev.Round; }
[UsedImplicitly]
[EventHandler]