Merge pull request #35 from ByDexterTR/main

v1.2.2.b6
This commit is contained in:
Juzlus 2026-07-07 01:01:45 +02:00 • committed by GitHub
commit b480247348
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 380 additions and 91 deletions

View file

@ -374,6 +374,29 @@ This plugin uses content from the following projects:
## 📋 Changelog
<details>
<summary><b>v1.2.2.b6</b></summary>
- #### General:
- ###### Performance: much faster round changes - only skills that were actually active in the previous round are reset, instead of every loaded skill (`DisableAll` dropped from ~89ms to ~5ms on a full server). [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Performance & memory: removed per-tick LINQ and skill-name allocations in the tick loop, made HUD skill lookups O(1), and moved translation color/button substitutions to load time. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### `ForceFullUpdate` now creates the network service once per broadcast, and the round-start view-angle guard only skips a genuine (0, 0, 0) spawn placeholder instead of over-skipping valid angles. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Debug logging is only installed when `DebugMode` is enabled and writes through a single reused writer instead of reopening the file for every line. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Player skill state unified to a single collection (removed a duplicate list), closing a rare desync/race on player disconnect. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Gamedata signatures are now resolved lazily and in isolation, so one broken signature after a CS2 update no longer takes down the whole utility layer. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Hardened many tick/transmit code paths against invalid player handles to prevent rare crashes. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### The skill description HUD honours "always show" / `-1` duration consistently across every assignment path. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- #### Skill improvements:
- ###### Jester (Joker):
- ###### Fixed the no-damage effect leaking onto a player after their skill was changed mid-round (e.g. stolen by Thief or removed by Deactivator) - the internal state is now cleared when the skill is disabled. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Jackal:
- ###### Fixed trail entities piling up over a round - the previous trail is now removed before a new one is spawned. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Falcon Eye / Third Eye / Spectator:
- ###### Camera state is now fully cleared when the skill is disabled. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
</details>
<details>
<summary><b>v1.2.2.b5</b></summary>

View file

@ -14,7 +14,6 @@ public class WASDMenuAPI
public static void LoadPlugin(BasePlugin basePlugin, bool hotReload)
{
var wasdMenuManager = new WasdManager();
basePlugin.RegisterEventHandler<EventPlayerActivate>((@event, info) =>
{
if (@event.Userid != null)
@ -46,7 +45,7 @@ public class WASDMenuAPI
public static void OnTick()
{
foreach (var player in Players.Values.Where(p => p.MainMenu != null))
foreach (var player in Players.Values.Where(p => p.MainMenu != null && p.Player?.IsValid == true))
{
if ((player.Buttons & PlayerButtons.Forward) == 0 && (player.Player.Buttons & PlayerButtons.Forward) != 0)
{

View file

@ -71,7 +71,9 @@
<Message Text="Copying compiled DLL from $(SourceDLL) to $(ServerDLL)" Importance="High" />
<ItemGroup>
<CompiledDLL Include="$(SourceDLL)\jRandomSkills.dll" />
<CompiledDLL Include="$(SourceDLL)\jRandomSkills.pdb" />
<CompiledDLL Include="$(SourceDLL)\WASDMenuAPI.dll" />
<CompiledDLL Include="$(SourceDLL)\WASDMenuAPI.pdb" />
<CompiledDLL Include="$(SourceDLL)\MaxMind.Db.dll" />
<CompiledDLL Include="$(SourceDLL)\Newtonsoft.Json.dll" />
</ItemGroup>

View file

@ -78,7 +78,7 @@ namespace src.command
player = PlayerManager.GetPlayerEvent(player);
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index);
if (playerInfo == null || playerInfo.IsDrawing) return;
var playerPawn = player.PlayerPawn.Value;
@ -150,7 +150,7 @@ namespace src.command
skillPlayer.Skill = skill.Skill;
skillPlayer.SpecialSkill = Skills.None;
Instance.SkillAction(skill.Skill.ToString(), "EnableSkill", [targetPlayer]);
skillPlayer.SkillDescriptionHudExpired = DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration);
skillPlayer.SkillDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration);
if (player == null)
{
@ -406,7 +406,7 @@ namespace src.command
if (player == null || !player.IsValid || player.PlayerPawn.Value == null || !player.PlayerPawn.Value.IsValid) return;
if (!string.IsNullOrEmpty(config.NormalCommands.HudCommand.Permissions) && !AdminManager.PlayerHasPermissions(player, config.NormalCommands.HudCommand.Permissions)) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index);
if (playerInfo == null) return;
playerInfo.DisplayHUD = !playerInfo.DisplayHUD;
@ -504,7 +504,7 @@ namespace src.command
Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [targetPlayer]);
skillPlayer.Skill = skill.Skill;
skillPlayer.SpecialSkill = src.player.Skills.None;
skillPlayer.SkillDescriptionHudExpired = DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration);
skillPlayer.SkillDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration);
if (skill.Skill == src.player.Skills.None)
Event.staticSkills.TryRemove(targetPlayer.Index, out _);
@ -570,6 +570,9 @@ namespace src.command
if (SkillsInfo.GetValue<bool>(skill, "active"))
Instance.SkillAction(skill.ToString()!, "LoadSkill");
SkillData.Invalidate();
Event.InvalidateFreezeDisabledCache();
if (player != null && player.IsValid)
player.PrintToChat($" {ChatColors.Green}{player.GetTranslation("reload")}");
else

View file

@ -19,16 +19,18 @@ namespace src
#pragma warning disable CS8618
public static jRandomSkills Instance { get; private set; }
#pragma warning restore CS8618
public ConcurrentBag<jSkill_PlayerInfo> SkillPlayer { get; set; } = [];
public IEnumerable<jSkill_PlayerInfo> SkillPlayer => PlayerManager.GetAllPlayers();
public Random Random { get; } = new Random();
public CCSGameRules? GameRules { get; set; }
private ConcurrentBag<string> ManifestResources { get; set; } = ["models/sprays/spray_plane.vmdl"];
public IWasdMenuManager? MenuManager;
// Skills that were enabled at least once this round; used to reset only those on round change (not all 124).
public static readonly ConcurrentDictionary<string, byte> ActiveSkillsThisRound = new();
public override string ModuleName => "[CS2] [ jRandomSkills ]";
public override string ModuleAuthor => "D3X, Juzlus";
public override string ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus.";
public override string ModuleVersion => "1.2.2.b5";
public override string ModuleVersion => "1.2.2.b6";
public override void Load(bool hotReload)
{
@ -85,6 +87,9 @@ namespace src
if (string.IsNullOrEmpty(skill))
return null;
if (methodName == "EnableSkill")
ActiveSkillsThisRound.TryAdd(skill, 0);
var method = _skillMethodCache.GetOrAdd((skill, methodName), key =>
{
string className = $"src.player.skills.{key.Skill}";
@ -109,7 +114,15 @@ namespace src
return type.GetMethod(key.Method, BindingFlags.Static | BindingFlags.Public);
});
return method?.Invoke(null, param);
if (method == null) return null;
if (!PerfLog.Enabled)
return method.Invoke(null, param);
long perfStart = PerfLog.Start();
var result = method.Invoke(null, param);
PerfLog.End($"SkillAction {skill}.{methodName}", perfStart, 2.0);
return result;
}
internal new void AddCommand(string name, string description, CommandInfo.CommandCallback handler)
@ -292,12 +305,9 @@ namespace src
}
}
/// <summary>
/// Gets a player info by index. O(n) lookup - consider caching if called frequently.
/// </summary>
internal jSkill_PlayerInfo? GetPlayerInfoByIndex(uint playerIndex)
{
return SkillPlayer.FirstOrDefault(p => p.PlayerIndex == playerIndex);
return PlayerManager.GetPlayerByIndex(playerIndex);
}
}
@ -329,6 +339,23 @@ namespace src
public static class SkillData
{
public static ConcurrentBag<jSkill_SkillInfo> Skills { get; } = [];
private static Dictionary<Skills, jSkill_SkillInfo>? _bySkill;
public static jSkill_SkillInfo? GetInfo(Skills skill)
{
var map = _bySkill;
if (map == null)
{
map = new Dictionary<Skills, jSkill_SkillInfo>();
foreach (var s in Skills)
map[s.Skill] = s;
_bySkill = map;
}
return map.TryGetValue(skill, out var info) ? info : null;
}
public static void Invalidate() => _bySkill = null;
}
public enum CS2ConsoleColors

View file

@ -13,10 +13,17 @@ namespace src.player
{
private static string sessionId = "00000";
private static readonly string debugFolder = Path.Combine(Instance.ModuleDirectory, "logs");
private static StreamWriter? _writer;
private static readonly object _writeLock = new();
public static void Load()
{
sessionId = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}";
lock (_writeLock) { _writer?.Dispose(); _writer = null; }
if (Config.LoadedConfig.DebugMode != true)
return;
Instance.RegisterEventHandler<EventPlayerConnectFull>((@event, info) =>
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
@ -131,13 +138,25 @@ namespace src.player
if (Config.LoadedConfig.DebugMode != true)
return;
// GetAllEntityIndexes();
lock (_writeLock)
{
_writer ??= CreateWriter();
_writer?.WriteLine($"[{DateTime.Now:dd.MM.yyyy HH:mm:ss}] {message}");
}
}
string filename = $"debug_{sessionId}.txt";
string path = Path.Combine(debugFolder, filename);
Directory.CreateDirectory(debugFolder);
File.AppendAllText(path, $"[{DateTime.Now:dd.MM.yyyy HH:mm:ss}] {message}{Environment.NewLine}", System.Text.Encoding.UTF8);
private static StreamWriter? CreateWriter()
{
try
{
Directory.CreateDirectory(debugFolder);
string path = Path.Combine(debugFolder, $"debug_{sessionId}.txt");
return new StreamWriter(path, append: true, System.Text.Encoding.UTF8) { AutoFlush = true };
}
catch
{
return null;
}
}
private static void GetAllEntityIndexes()

View file

@ -0,0 +1,100 @@
using src.utils;
using System.Collections.Concurrent;
using System.Diagnostics;
using static src.jRandomSkills;
namespace src.player
{
public static class PerfLog
{
private static readonly string logsFolder = Path.Combine(Instance.ModuleDirectory, "logs");
private static readonly string sessionId = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}";
private static StreamWriter? _writer;
private static readonly object _writeLock = new();
public static bool Enabled => Config.LoadedConfig.PerfMode;
private static bool _headerWritten;
public static long Start()
{
if (!Enabled) return 0;
// Write a header on the first measurement so the perf file appears immediately
// when PerfMode is active - makes "is it working?" instantly visible.
if (!_headerWritten)
{
_headerWritten = true;
Write($"PerfMode enabled (plugin v{Instance.ModuleVersion})");
}
return Stopwatch.GetTimestamp();
}
// One-shot measurement: logs "label took X.XXms" when the elapsed time reaches the threshold.
public static void End(string label, long startTimestamp, double thresholdMs = 1.0)
{
if (startTimestamp == 0 || !Enabled) return;
double ms = (Stopwatch.GetTimestamp() - startTimestamp) * 1000.0 / Stopwatch.Frequency;
if (ms < thresholdMs) return;
Write($"{label} took {ms:F2}ms");
}
private sealed class Aggregate
{
public double TotalMs;
public double MaxMs;
public int Count;
public DateTime WindowStart = DateTime.Now;
}
private static readonly ConcurrentDictionary<string, Aggregate> _aggregates = new();
// Per-tick measurement: accumulates and logs an avg/max summary every few seconds,
// so tick paths do not produce one log line per tick.
public static void Sample(string label, long startTimestamp, double reportSeconds = 5.0, double maxThresholdMs = 0.5)
{
if (startTimestamp == 0 || !Enabled) return;
double ms = (Stopwatch.GetTimestamp() - startTimestamp) * 1000.0 / Stopwatch.Frequency;
var agg = _aggregates.GetOrAdd(label, _ => new Aggregate());
lock (agg)
{
agg.TotalMs += ms;
agg.Count++;
if (ms > agg.MaxMs) agg.MaxMs = ms;
if ((DateTime.Now - agg.WindowStart).TotalSeconds < reportSeconds) return;
if (agg.MaxMs >= maxThresholdMs)
Write($"{label} avg={agg.TotalMs / agg.Count:F2}ms max={agg.MaxMs:F2}ms samples={agg.Count}");
agg.TotalMs = 0;
agg.MaxMs = 0;
agg.Count = 0;
agg.WindowStart = DateTime.Now;
}
}
private static void Write(string message)
{
lock (_writeLock)
{
try
{
if (_writer == null)
{
Directory.CreateDirectory(logsFolder);
_writer = new StreamWriter(Path.Combine(logsFolder, $"perf_{sessionId}.txt"), append: true, System.Text.Encoding.UTF8) { AutoFlush = true };
}
_writer.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [PERF] {message}");
}
catch
{
}
}
}
}
}

View file

@ -417,23 +417,53 @@ namespace src.player
}
}
private static readonly Dictionary<Skills, string> _skillNames =
Enum.GetValues<Skills>().ToDictionary(s => s, s => s.ToString());
private static readonly HashSet<Skills> _activeSkillsSet = [];
private static readonly List<Skills> _activeSkillsList = [];
private static readonly Comparison<Skills> _tickOrderCmp = (a, b) => TickOrder(a).CompareTo(TickOrder(b));
private static HashSet<Skills>? _freezeDisabledSkills;
// AreaReaper and ChillOut depend on other skills' tick results, so they must tick last.
private static int TickOrder(Skills s) => s == Skills.AreaReaper ? 2 : s == Skills.ChillOut ? 1 : 0;
private static HashSet<Skills> BuildFreezeDisabledSkills()
{
var set = new HashSet<Skills>();
foreach (var s in SkillData.Skills)
if (SkillsInfo.GetValue<bool>(s.Skill, "disableOnFreezeTime"))
set.Add(s.Skill);
return set;
}
public static void InvalidateFreezeDisabledCache() => _freezeDisabledSkills = null;
private static void OnTick()
{
long perfStart = PerfLog.Start();
lock (setLock)
{
var activeSkills = Instance.SkillPlayer
.Where(p => !p.IsDrawing)
.Select(p => p.Skill)
.Distinct()
.OrderBy(skill => skill.ToString() == "AreaReaper")
.ThenBy(skill => skill.ToString() == "ChillOut");
_activeSkillsSet.Clear();
_activeSkillsList.Clear();
foreach (var p in Instance.SkillPlayer)
{
if (p.IsDrawing) continue;
if (_activeSkillsSet.Add(p.Skill))
_activeSkillsList.Add(p.Skill);
}
foreach (var skill in activeSkills)
if (SkillsInfo.GetValue<bool>(skill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime())
continue;
else
Instance.SkillAction(skill.ToString(), "OnTick");
_activeSkillsList.Sort(_tickOrderCmp);
bool freeze = SkillUtils.IsFreezeTime();
_freezeDisabledSkills ??= BuildFreezeDisabledSkills();
foreach (var skill in _activeSkillsList)
{
if (freeze && _freezeDisabledSkills.Contains(skill)) continue;
Instance.SkillAction(_skillNames[skill], "OnTick");
}
}
PerfLog.Sample("OnTick(skills)", perfStart);
}
private static void OnPlayerConnectedBot(int playerSlot)
@ -466,7 +496,6 @@ namespace src.player
DisplayHUD = true,
SkillUsed = false,
};
Instance.SkillPlayer.Add(playerInfo);
PlayerManager.Register(playerInfo);
}
}
@ -502,8 +531,6 @@ namespace src.player
Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]);
var items = Instance.SkillPlayer.ToList();
Instance.SkillPlayer = [.. items.Where(p => p.PlayerIndex != player.Index)];
PlayerManager.UnregisterPlayer(player.Index);
EntityManager.DestroyPlayerEntities(player.Index);
@ -568,7 +595,7 @@ namespace src.player
{
lock (setLock)
{
bool isWarmup = Instance.GameRules != null && Instance.GameRules.WarmupPeriod == true;
bool isWarmup = Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true;
isTransmitRegistered = false;
Instance.AddTimer(.1f, () => DisableAll(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
@ -593,6 +620,13 @@ namespace src.player
}
private static void DisableAll()
{
long perfStart = PerfLog.Start();
DisableAllCore();
PerfLog.End("DisableAll total", perfStart, 2.0);
}
private static void DisableAllCore()
{
lock (setLock)
{
@ -606,6 +640,10 @@ namespace src.player
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) continue;
ActiveSkillsThisRound.TryAdd(playerInfo.Skill.ToString(), 0);
if (playerInfo.SpecialSkill != noneSkill.Skill)
ActiveSkillsThisRound.TryAdd(playerInfo.SpecialSkill.ToString(), 0);
Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [player]);
playerInfo.Skill = noneSkill.Skill;
@ -617,8 +655,9 @@ namespace src.player
RestorePlayer(player);
}
foreach (var skill in SkillData.Skills)
Instance.SkillAction(skill.Skill.ToString(), "NewRound");
foreach (var skillName in ActiveSkillsThisRound.Keys)
Instance.SkillAction(skillName, "NewRound");
ActiveSkillsThisRound.Clear();
}
}
@ -649,6 +688,7 @@ namespace src.player
EntityManager.DestroyAllTracked();
foreach (var skill in SkillData.Skills)
Instance.SkillAction(skill.Skill.ToString(), "NewRound");
ActiveSkillsThisRound.Clear();
playersSkills.Clear();
staticSkills.Clear();
@ -657,7 +697,6 @@ namespace src.player
tSkill = noneSkill;
allSkill = noneSkill;
Instance.SkillPlayer.Clear();
PlayerManager.Clear();
ConVar.Find("sv_legacy_jump")?.SetValue("1");
@ -712,6 +751,14 @@ namespace src.player
}
private static HookResult PlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
long perfStart = PerfLog.Start();
var result = PlayerDeathCore(@event, info);
PerfLog.End("PlayerDeath total", perfStart, 2.0);
return result;
}
private static HookResult PlayerDeathCore(EventPlayerDeath @event, GameEventInfo info)
{
lock (setLock)
{
@ -817,13 +864,22 @@ namespace src.player
}
private static void SetSkill()
{
long perfStart = PerfLog.Start();
SetSkillCore();
PerfLog.End("SetSkill total", perfStart, 2.0);
}
private static void SetSkillCore()
{
setSkillTimer = null;
lock (setLock)
{
if (Instance == null) return;
if (Instance.GameRules != null && Instance.GameRules.WarmupPeriod == true)
// GameRules can be null for a short window right after plugin load/hot-reload;
// treat that as "not ready" so skills are never assigned during warmup by accident.
if (Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true)
{
setSkillTimer?.Kill();
return;
@ -1113,16 +1169,18 @@ namespace src.player
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
Debug.WriteToDebug($"Player {skillPlayer.PlayerName} has got the skill \"{player.GetSkillName(randomSkill.Skill)}\".");
skillPlayer.SkillDescriptionHudExpired = DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration);
skillPlayer.SkillDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration);
}
}
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
{
long perfStart = PerfLog.Start();
lock (setLock)
{
DispatchToActiveSkills("CheckTransmit", infoList);
}
PerfLog.Sample("CheckTransmit", perfStart);
}
public static void UpdateSkillHUD(CCSPlayerController? player, string? headerLine, string? centerLine, string? extraLine, bool isDescription)

View file

@ -17,11 +17,13 @@ namespace src.player
UpdateGameRules();
if (Server.TickCount % 2 != 0) return;
long perfStart = PerfLog.Start();
foreach (var player in Utilities.GetPlayers())
{
if (player != null && player.IsValid)
UpdatePlayerHud(player);
}
PerfLog.Sample("OnTick(hud)", perfStart);
});
Instance.RegisterListener<OnMapStart>(OnMapStart);
@ -61,7 +63,7 @@ namespace src.player
{
if (player == null || !player.IsValid || player.IsBot) return;
var skillPlayer = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var skillPlayer = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index);
if (skillPlayer == null || !skillPlayer.DisplayHUD) return;
var now = DateTime.Now;
@ -72,7 +74,7 @@ namespace src.player
string skillLine = string.Empty;
string remainingLine = string.Empty;
bool showDescriptionHUD = skillPlayer.SkillDescriptionHudExpired >= now;
bool showDescriptionHUD = skillPlayer.SkillDescriptionHudExpired >= now || Config.LoadedConfig.DisplayAlwaysDescription;
bool isDescription = true;
var skills = SkillData.Skills;
@ -99,7 +101,7 @@ namespace src.player
{
if (player.PawnIsAlive)
{
var skillInfo = skills.FirstOrDefault(s => s.Skill == skillPlayer.Skill);
var skillInfo = SkillData.GetInfo(skillPlayer.Skill);
if (skillInfo != null)
{
@ -139,9 +141,9 @@ namespace src.player
var observedSkill = PlayerManager.GetPlayerByIndex(observedEvent.Index);
if (observedSkill == null) return;
var observedSkillInfo = skills.FirstOrDefault(s => s.Skill == observedSkill.Skill);
var observedSkillInfo = SkillData.GetInfo(observedSkill.Skill);
var observedSpecialInfo = observedSkill.SpecialSkill != Skills.None
? skills.FirstOrDefault(s => s.Skill == observedSkill.SpecialSkill)
? SkillData.GetInfo(observedSkill.SpecialSkill)
: null;
string primaryName = player.GetSkillName(observedSkill.Skill, observedSkill.SkillChance);

View file

@ -47,7 +47,7 @@ namespace src.player.skills
var victim = victimController.As<CCSPlayerController>();
if (victim == null || !victim.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(victim)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(victim)?.Index ?? victim.Index));
if (playerInfo == null) return;
if (playerInfo.Skill == skillName && victim.PawnIsAlive)

View file

@ -36,7 +36,7 @@ namespace src.player.skills
if (victim.Index == attacker.Index || victim.Team == attacker.Team)
return;
var attackerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(attacker)!.Index);
var attackerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(attacker)?.Index ?? attacker.Index));
if (attackerInfo?.Skill != skillName) return;
SkillUtils.TakeHealth(victim!.PlayerPawn.Value, SkillsInfo.GetValue<int>(skillName, "damageDeal"));
@ -57,7 +57,7 @@ namespace src.player.skills
var player = pawn.Controller.Value.As<CCSPlayerController>();
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
decoys.TryAdd(decoy.Index, 0);

View file

@ -22,7 +22,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill == skillName)
GiveBunnyHop(player);
}

View file

@ -77,7 +77,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo == null) continue;
var pawn = player.PlayerPawn.Value;

View file

@ -53,6 +53,7 @@ namespace src.player.skills
if (player == null) return;
ChangeCamera(player, true);
EntityManager.DestroyPlayerEntities(player.Index);
cameras.TryRemove(player.Index, out _);
}
public static void OnTick()

View file

@ -50,7 +50,7 @@ namespace src.player.skills
var victim = victimController.As<CCSPlayerController>();
if (victim == null || !victim.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(attacker)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(attacker)?.Index ?? attacker.Index));
if (playerInfo?.Skill != skillName || attacker!.Team != victim!.Team) return;
float damage = param2.Damage;

View file

@ -42,7 +42,7 @@ namespace src.player.skills
foreach (var (info, player) in infoList)
{
if (player == null) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
var observedPlayer = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == player?.Pawn?.Value?.ObserverServices?.ObserverTarget?.Value?.Handle);
var observerInfo = observedPlayer != null ? PlayerManager.GetPlayerByIndex(observedPlayer.Index) : null;

View file

@ -28,7 +28,7 @@ namespace src.player.skills
var player = pawn.Controller.Value.As<CCSPlayerController>();
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
grenade.Bounces = 555;
}

View file

@ -60,7 +60,7 @@ namespace src.player.skills
var player = pawn.Controller.Value.As<CCSPlayerController>();
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
Server.NextFrame(() =>

View file

@ -127,7 +127,7 @@ namespace src.player.skills
if (pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return;
var player = pawn.Controller.Value.As<CCSPlayerController>();
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
Vector pos = new(grenade.AbsOrigin?.X, grenade.AbsOrigin?.Y, grenade.AbsOrigin?.Z);

View file

@ -46,7 +46,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
bool isJackalOwner = playerInfo?.Skill == skillName;
if (!isJackalOwner)
@ -90,6 +90,11 @@ namespace src.player.skills
if (playerPawn == null || !playerPawn.IsValid || playerPawn.AbsOrigin == null
|| playerPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE || playerPawn.Health <= 0) return; ;
foreach (var old in EntityManager.GetPlayerEntities(player.Index, "prop_physics_multiplayer"))
EntityManager.DestroyEntity(old);
foreach (var old in EntityManager.GetPlayerEntities(player.Index, "particle_system"))
EntityManager.DestroyEntity(old);
var relay = EntityManager.CreateTrackedPhysicsProp(player.Index);
if (relay == null || !relay.IsValid) return;

View file

@ -53,16 +53,18 @@ namespace src.player.skills
var jester = GetJesterInfo(player.Index);
if (jester == null) return;
jester.Active = false;
if (jester.Timer != null)
{
jester.Timer.Kill();
jester.Timer = null;
}
Server.NextWorldUpdate(() =>
{
if (jester.Timer != null)
{
jester.Timer?.Kill();
jester.Timer = null;
}
SkillUtils.ResetPrintHTML(player);
SetPlayerColor(player, true);
jesters.TryRemove(player.Index, out _);
});
}

View file

@ -88,7 +88,7 @@ namespace src.player.skills
if (pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return;
var player = pawn.Controller.Value.As<CCSPlayerController>();
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
nades.TryAdd(grenade.Index, 0);

View file

@ -39,7 +39,7 @@ namespace src.player.skills
var player = @event.Userid;
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
UpdateNinja(player);
@ -50,7 +50,7 @@ namespace src.player.skills
var player = @event.Userid;
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
UpdateNinja(player);
@ -118,7 +118,7 @@ namespace src.player.skills
UpdateNinja(PlayerManager.GetPlayerFromEvent(player));
var props = EntityManager.GetPlayerEntities(PlayerManager.GetPlayerEvent(player)!.Index, "empty_prop");
var props = EntityManager.GetPlayerEntities((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index), "empty_prop");
if (props.Count == 0) continue;
var prop = Utilities.GetEntityFromIndex<CDynamicProp>((int)props[0]);

View file

@ -32,9 +32,10 @@ namespace src.player.skills
if (attackerPawn == null || attackerPawn.Controller?.Value == null || victimPawn == null || victimPawn.Controller?.Value == null)
return;
CCSPlayerController attacker = PlayerManager.GetPlayerEvent(attackerPawn.Controller.Value.As<CCSPlayerController>())!;
var attacker = PlayerManager.GetPlayerEvent(attackerPawn.Controller.Value.As<CCSPlayerController>());
if (attacker == null || !attacker.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(attacker)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex(attacker.Index);
if (playerInfo == null) return;
if (playerInfo.Skill == skillName)

View file

@ -27,7 +27,7 @@ namespace src.player.skills
var player = Utilities.GetPlayers().FirstOrDefault(p => p.Pawn?.Value != null && p.Pawn.Value.IsValid && p.Pawn.Value.Index == userIndex);
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
um.Recipients.Clear();

View file

@ -51,6 +51,7 @@ namespace src.player.skills
if (player == null) return;
ChangeCamera(player, true);
EntityManager.DestroyPlayerEntities(player.Index);
cameras.TryRemove(player.Index, out _);
}
public static void OnTick()

View file

@ -39,6 +39,7 @@ namespace src.player.skills
if (player == null) return;
ChangeCamera(player, true);
EntityManager.DestroyPlayerEntities(player.Index);
cameras.TryRemove(player.Index, out _);
}
public static void OnTick()

View file

@ -388,7 +388,7 @@ namespace src.player.skills
continue;
}
bool isOwner = knifeOwnerIndex == PlayerManager.GetPlayerEvent(player)!.Index;
bool isOwner = knifeOwnerIndex == (PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index);
bool isObservingOwner = observedPlayerIndex.HasValue && observedPlayerIndex.Value == knifeOwnerIndex;
if (!isOwner && !isObservingOwner)

View file

@ -60,7 +60,7 @@ namespace src.player.skills
var player = pawn.Controller.Value.As<CCSPlayerController>();
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
Server.NextFrame(() =>

View file

@ -32,7 +32,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) continue;
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
var observedPlayer = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == player?.Pawn?.Value?.ObserverServices?.ObserverTarget?.Value?.Handle);
var observerInfo = observedPlayer != null ? PlayerManager.GetPlayerByIndex(observedPlayer.Index) : null;

View file

@ -39,7 +39,7 @@ namespace src.player.skills
if (pawn == null || !pawn.IsValid || pawn.Controller == null || !pawn.Controller.IsValid || pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return;
var player = pawn.Controller.Value.As<CCSPlayerController>();
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName || Instance.GameRules == null) return;
var roundTime = SkillsInfo.GetValue<int>(skillName, "changeRoundTime");

View file

@ -62,7 +62,7 @@ namespace src.player.skills
if (pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return;
var player = pawn.Controller.Value.As<CCSPlayerController>();
var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index);
var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index));
if (playerInfo?.Skill != skillName) return;
Server.NextFrame(() => {

View file

@ -80,6 +80,7 @@ namespace src.utils
public bool EnableBotSkills { get; set; }
public bool EnableBotKickDebug { get; set; }
public bool DebugMode { get; set; }
public bool PerfMode { get; set; }
public string? AlternativeSkillButton { get; set; }
public float SkillTimeBeforeStart { get; set; }
public float SkillHudDuration { get; set; }
@ -107,6 +108,7 @@ namespace src.utils
EnableBotSkills = true;
EnableBotKickDebug = false;
DebugMode = false;
PerfMode = false;
AlternativeSkillButton = null;
SkillTimeBeforeStart = 7;
SkillHudDuration = -1;

View file

@ -55,7 +55,18 @@ namespace src.utils
var translations = JsonConvert.DeserializeObject<ConcurrentDictionary<string, string>>(jsonText);
if (translations != null)
{
string redColor = ChatColors.Red.ToString();
string? altButton = Config.LoadedConfig.AlternativeSkillButton;
foreach (var tkey in translations.Keys.ToList())
{
var val = translations[tkey].Replace("CHATCOLORS.RED", redColor);
if (!string.IsNullOrEmpty(altButton))
val = val.Replace("css_useSkill", $"css_useSkill/{altButton}");
translations[tkey] = val;
}
_translations.AddOrUpdate(code, translations, (k, v) => translations);
}
}
public static bool HasTranslation(string code)
@ -150,9 +161,6 @@ namespace src.utils
else
{
string output = args.Length == 0 ? translation : string.Format(translation, args);
output = output.Replace("CHATCOLORS.RED", ChatColors.Red.ToString());
if (Config.LoadedConfig.AlternativeSkillButton != null)
output = output.Replace("css_useSkill", $"css_useSkill/{Config.LoadedConfig.AlternativeSkillButton}");
if (Illiterate.CheckIlliterateSkill(player))
return Illiterate.GetRandomText(output)!;
@ -164,6 +172,8 @@ namespace src.utils
return key;
}
private static bool _geoLiteBroken = false;
private static string GetLangCode(CCSPlayerController? player)
{
if (player == null || !player.IsValid || player.IsBot) return defaultLangCode;
@ -172,10 +182,24 @@ namespace src.utils
if (!string.IsNullOrEmpty(fileLangCode))
return fileLangCode;
if (Config.LoadedConfig.LanguageSystem.DisableGeoLite == true)
if (Config.LoadedConfig.LanguageSystem.DisableGeoLite == true || _geoLiteBroken)
return defaultLangCode;
string? geoliteLandCode = GetLangCodeFromDatabase(GetPlayerIP(player)) ?? defaultLangCode;
string? geoliteLandCode;
try
{
geoliteLandCode = GetLangCodeFromDatabase(GetPlayerIP(player)) ?? defaultLangCode;
}
catch (Exception ex)
{
// MaxMind.Db can fail to load (e.g. hot-reload while the old AssemblyLoadContext is
// unloading) or the .mmdb can be corrupt; fall back to the default language and stop
// trying for the rest of the session instead of throwing every HUD tick.
_geoLiteBroken = true;
Server.PrintToConsole($"[jRandomSkills] GeoLite lookup disabled for this session: {ex.GetType().Name}: {ex.Message}");
geoliteLandCode = defaultLangCode;
}
ChangePlayerLanguage(player, geoliteLandCode);
return geoliteLandCode;
}
@ -189,6 +213,9 @@ namespace src.utils
return parts.Length > 1 ? parts[0] : playerIP;
}
// NoInlining keeps the MaxMind.Db type references out of GetLangCode, so an assembly-load
// failure surfaces inside the try/catch at the call site instead of when GetLangCode is JITed.
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private static string? GetLangCodeFromDatabase(string? playerIP)
{
if (string.IsNullOrEmpty(playerIP)) return null;

View file

@ -21,10 +21,21 @@ namespace src.utils
{
public static class SkillUtils
{
private static readonly MemoryFunctionWithReturn<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int> HEGrenadeProjectile_CreateFunc = new(GameData.GetSignature("HEGrenadeProjectile_CreateFunc"));
private static readonly MemoryFunctionWithReturn<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int, int, CSmokeGrenadeProjectile> SmokeGrenadeProjectile_CreateFunc = new(GameData.GetSignature("SmokeGrenadeProjectile_CreateFunc"));
private static readonly MemoryFunctionVoid<nint, float, RoundEndReason, nint, nint> TerminateRoundFunc = new(GameData.GetSignature("CCSGameRules_TerminateRound"));
private static readonly MemoryFunctionVoid<CBasePlayerPawn, QAngle> SnapViewAngles = new(GameData.GetSignature("SnapViewAngles"));
private static Lazy<T?> LazySig<T>(string name, Func<string, T> factory) where T : class =>
new(() =>
{
try { return factory(GameData.GetSignature(name)); }
catch (Exception ex) { Server.PrintToConsole($"[jRandomSkills] gamedata signature '{name}' could not be resolved: {ex.Message}"); return null; }
});
private static readonly Lazy<MemoryFunctionWithReturn<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int>?> HEGrenadeProjectile_CreateFunc =
LazySig<MemoryFunctionWithReturn<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int>>("HEGrenadeProjectile_CreateFunc", s => new(s));
private static readonly Lazy<MemoryFunctionWithReturn<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int, int, CSmokeGrenadeProjectile>?> SmokeGrenadeProjectile_CreateFunc =
LazySig<MemoryFunctionWithReturn<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int, int, CSmokeGrenadeProjectile>>("SmokeGrenadeProjectile_CreateFunc", s => new(s));
private static readonly Lazy<MemoryFunctionVoid<nint, float, RoundEndReason, nint, nint>?> TerminateRoundFunc =
LazySig<MemoryFunctionVoid<nint, float, RoundEndReason, nint, nint>>("CCSGameRules_TerminateRound", s => new(s));
private static readonly Lazy<MemoryFunctionVoid<CBasePlayerPawn, QAngle>?> SnapViewAngles =
LazySig<MemoryFunctionVoid<CBasePlayerPawn, QAngle>>("SnapViewAngles", s => new(s));
// private static readonly int collisionRulesChangedOffset = GameData.GetOffset("CBaseEntity_CollisionRulesChanged");
public static void PrintToChat(CCSPlayerController player, string? msg, string border = "tb", string? title = null)
@ -74,6 +85,8 @@ namespace src.utils
weapon.Value.Clip1 = ammo;
Utilities.SetStateChanged(weapon.Value, "CBasePlayerWeapon", "m_iClip1");
if (ammo == 1) return;
jRandomSkills.Instance.AddTimer(.1f, () =>
{
if (weapon == null || !weapon.IsValid || weapon.Value == null || !weapon.Value.IsValid) return;
@ -169,7 +182,7 @@ namespace src.utils
public static void Look(this CBasePlayerPawn pawn, QAngle angle)
{
if (pawn == null || !pawn.IsValid) return;
SnapViewAngles.Invoke(pawn, angle);
SnapViewAngles.Value?.Invoke(pawn, angle);
}
public static CBeam? CreateLine(Vector start, Vector end, Color color, uint ownerPlayerIndex = EntityManager.SystemOwnerIndex)
@ -264,12 +277,12 @@ namespace src.utils
public static void CreateHEGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int teamNum)
{
HEGrenadeProjectile_CreateFunc.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 44, teamNum);
HEGrenadeProjectile_CreateFunc.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 44, teamNum);
}
public static void CreateSmokeGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int teamNum)
{
SmokeGrenadeProjectile_CreateFunc.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 45, teamNum);
SmokeGrenadeProjectile_CreateFunc.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 45, teamNum);
}
public static bool TakeHealth(CCSPlayerPawn? pawn, int damage)
@ -320,7 +333,7 @@ namespace src.utils
return EntityManager.CreateTrackedTrigger(ownerPlayerIndex, name, radius, pos);
}
public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null)
public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null, INetworkGameServer? networkGameServer = null)
{
if (player == null || !player.IsValid) return;
@ -329,14 +342,15 @@ namespace src.utils
QAngle lastAngle = new(pawn.V_angle.X, pawn.V_angle.Y, pawn.V_angle.Z);
INetworkServerService networkServerService = new();
INetworkGameServer networkGameServer = networkServerService.GetIGameServer();
networkGameServer ??= new INetworkServerService().GetIGameServer();
var client = networkGameServer.GetClientBySlot(player.Slot);
if (client == null) return;
client.ForceFullUpdate();
if (lastAngle.Y == 0) return;
// Only skip the angle restore when the captured view is a spawn-time (0,0,0) placeholder;
// a genuine angle with a single zero component (e.g. yaw exactly 0) must still be restored.
if (lastAngle.X == 0 && lastAngle.Y == 0 && lastAngle.Z == 0) return;
uint playerIndex = player.Index;
@ -368,8 +382,9 @@ namespace src.utils
lastForceFullUpdateAll = tickCount;
var playersToRestore = new List<(uint PlayerIndex, QAngle LastAngle)>();
INetworkGameServer networkGameServer = new INetworkServerService().GetIGameServer();
foreach (var player in Utilities.GetPlayers())
ForceFullUpdate(player, playersToRestore);
ForceFullUpdate(player, playersToRestore, networkGameServer);
if (playersToRestore.Count <= 0) return;
@ -639,7 +654,7 @@ namespace src.utils
{
if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return;
UpdateServerTeamScores(ctScore, tScore);
TerminateRoundFunc.Invoke(jRandomSkills.Instance.GameRules.Handle, 5f, roundEndReason, 0, 0);
TerminateRoundFunc.Value?.Invoke(jRandomSkills.Instance.GameRules.Handle, 5f, roundEndReason, 0, 0);
}
public static void TerminateRound(CsTeam winnerTeam)

View file

@ -8,6 +8,7 @@
"EnableBotSkills": true,
"EnableBotKickDebug": false,
"DebugMode": false,
"PerfMode": false,
"AlternativeSkillButton": null,
"SkillTimeBeforeStart": 7.0,
"SkillHudDuration": -1.0,