jRandomSkills/jRandomSkills - SRC Files/src/utils/SkillUtils.cs
ByDexter 21e702654d Fix leaked skill state, second chance revives, and unrestored server cvars
General:
- Jester's no-damage state no longer applies to players who do not hold the
  skill. The shared health helper checked Jester's internal table without
  verifying the current assignment, so a single stale entry silently cancelled
  damage across every skill that deals damage through it.
- Jester's per-round reset no longer aborts when the holder is dead, which
  previously left the table populated for the following rounds.
- The per-round reset now covers every skill used on the current map instead of
  only the ones held in the round that just ended, so a skill nobody drew still
  clears state left over from an earlier round.
- Skills that track another player as a target are now notified when that player
  disconnects. Controller indexes are reused, so a leftover entry applied to
  whoever connected into that index next. Affects Poison, Deaf, Darkness,
  Glitch, Magnifier, Legless, No Rifles, Jammer and Jester.
- Aimbot no longer retains raw native pointers after a hit is handled. They were
  never cleared and were written back to later, which could corrupt memory the
  engine had already freed.
- An exception inside a skill's OnTick no longer cancels the tick of every skill
  after it in the same frame.
- Cooldowns configured low enough to truncate to zero no longer throw (Poison,
  Regeneration, Hot Bomb, Healing Smoke, Toxic Smoke).
- Round changes are cheaper: Long Knife no longer detaches a detour it never
  attached, and Wild Throw skips its player sweep when it has nothing to unwind.

Skill improvements:
- Second Chance: now survives several lethal hits landing in the same tick
  (shotgun, two attackers) and restores health inside the damage hook instead of
  a frame later. It also runs after skills that modify damage, so One-Shot and
  Soldier are seen at their final value instead of the raw one.
- Spectator: returning to your own view no longer leaves the camera handle set,
  which made the tick handler reset the weapon cooldown every frame and play as
  rapid fire. The manual toggle is unchanged.
- Radar Hack: players hidden behind a replacement model (Chicken) are shown on
  the radar again. Only Ghost, Ninja and C4 Camouflage hide the pawn itself, so
  render alpha alone no longer decides.
- Focus: weapon_accuracy_nospread is reference counted, so one holder losing the
  skill no longer removes the effect from the others still holding it.
- Friendly Fire: mp_autokick is restored at the end of the round instead of
  staying disabled for the rest of the map, and is no longer executed on every
  friendly hit.
- Reactive Armor / Iana: no longer restore health for a player who no longer
  holds the skill.
- Free Planter / Short Fuse: each restores mp_c4timer only when it actually
  changed it, so overlapping rounds cannot write back the wrong value.
- Illusionist: the replica movement timer now stops on map change.
- Ghost / Ninja / C4 Camouflage: the bomb is located once per transmit instead of
  once per receiver and hidden player, and the pass is skipped when nobody is
  hidden.
2026-07-21 19:44:13 +03:00

811 lines
No EOL
36 KiB
C#

using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.UserMessages;
using CounterStrikeSharp.API.Modules.Utils;
using jRandomSkills.src.utils;
using src.player;
using src.player.skills;
using System.Collections.Concurrent;
using System.Drawing;
using System.Globalization;
using System.Runtime.InteropServices;
using WASDMenuAPI.Classes;
using WASDSharedAPI;
namespace src.utils
{
public static class SkillUtils
{
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)
{
if (!player.IsValid) return;
var config = Config.LoadedConfig.ChatMessage;
float maxWidth = config.MaxWidth;
char symbol = config.LineSymbol;
if (string.IsNullOrEmpty(title)) title = player.GetTranslation("jRandomSkills");
if (Illiterate.CheckIlliterateSkill(player))
msg = Illiterate.GetRandomText(msg);
if (border.Contains('t') && config.LineShow)
player.PrintToChat($" {MeansureString.GetTextDashed($"{(config.TagFormat.Contains("{TAG}") ? config.TagFormat.Replace("{TAG}", title) : $"\u0002◢◆◤ {title} ◥◆◣")}", maxWidth, symbol, config.LineColor)}");
if (!string.IsNullOrEmpty(msg) && config.InfoMessageShow)
player.PrintToChat($" {config.InfoSkillColor} {msg.Replace("\x02", config.InfoPlayerNameColor).Replace("\x06", config.InfoSkillColor)}");
if (border.Contains('b') && config.LineShow)
player.PrintToChat($" {MeansureString.GetTextDashed("", maxWidth, symbol, config.LineColor)}");
}
public static bool IsFreezeTime()
{
return jRandomSkills.Instance?.GameRules?.FreezePeriod == true;
}
public static void RegisterSkill(Skills skill, string color, bool display = true)
{
if (!SkillData.Skills.Any(s => s.Skill == skill))
SkillData.Skills.Add(new jSkill_SkillInfo(skill, color, display));
}
public static void UpdateGrenadeCount(CCSPlayerController player, CsItem item, int ammo)
{
string? itemString = EnumUtils.GetEnumMemberAttributeValue(item);
if (string.IsNullOrWhiteSpace(itemString)) return;
if (player == null || !player.IsValid || player.PlayerPawn.Value == null || !player.PlayerPawn.Value.IsValid) return;
if (player.PlayerPawn.Value.WeaponServices == null) return;
var weapon = player.PlayerPawn.Value.WeaponServices.MyWeapons
.FirstOrDefault(w => w != null && w.IsValid && w.Value != null && w.Value.IsValid && !string.IsNullOrEmpty(w.Value.DesignerName) && w.Value.DesignerName == itemString);
if (weapon == null || !weapon.IsValid || weapon.Value == null || !weapon.Value.IsValid) return;
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;
weapon.Value.Clip1 = 1;
}, TimerFlags.STOP_ON_MAPCHANGE);
}
public static void TryGiveWeapon(CCSPlayerController player, CsItem item, int count = 1, bool existValidator = true)
{
string? itemString = EnumUtils.GetEnumMemberAttributeValue(item);
if (string.IsNullOrWhiteSpace(itemString)) return;
if (player == null || !player.IsValid || player.PlayerPawn.Value == null || !player.PlayerPawn.Value.IsValid) return;
if (player.PlayerPawn.Value.WeaponServices == null) return;
var exists = player.PlayerPawn.Value.WeaponServices.MyWeapons
.FirstOrDefault(w => w != null && w.IsValid && w.Value != null && w.Value.IsValid && w.Value.DesignerName == itemString);
if (exists == null || !existValidator)
for (int i = 0; i < count; i++)
player.GiveNamedItem(item);
}
public static double GetDistance(Vector vector1, Vector vector2)
{
return Math.Sqrt(Math.Pow(vector2.X - vector1.X, 2) + Math.Pow(vector2.Y - vector1.Y, 2) + Math.Pow(vector2.Z - vector1.Z, 2));
}
public static float Distance(this Vector vector1, Vector vector2)
{
return (float)GetDistance(vector1, vector2);
}
public static float Dot(this Vector vector1, Vector vector2)
{
return (vector1.X * vector2.X) + (vector1.Y * vector2.Y) + (vector1.Z * vector2.Z);
}
public static Vector Normalize(this Vector vector)
{
float length = vector.Length();
if (length > 0)
return new Vector(vector.X / length, vector.Y / length, vector.Z / length);
return Vector.Zero;
}
public static string SecondsToTimer(int totalSeconds)
{
if (totalSeconds <= 0) return "00:00";
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return $"{minutes:D2}:{seconds:D2}";
}
public static void SafeKillEntity<T>(uint? index) where T : CBaseEntity
{
if (index == null) return;
EntityManager.DestroyEntity(index.Value);
}
public static bool IsValid<T>(this CHandle<T>? handle) where T : NativeEntity
{
return handle != null && handle.IsValid && handle.Value != null;
}
public static bool IsValid(this CBaseEntity? ent)
{
return ent != null && ent.IsValid;
}
public static bool CheckPlayer(this CCSPlayerController? player)
{
return player != null
&& player.IsValid
&& player.PlayerPawn?.Value?.IsValid() == true
&& player.PawnIsAlive
&& (player?.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist);
}
public static Vector GetForwardVector(QAngle angles)
{
float pitch = -angles.X * (float)(Math.PI / 180);
float yaw = angles.Y * (float)(Math.PI / 180);
float x = (float)(Math.Cos(pitch) * Math.Cos(yaw));
float y = (float)(Math.Cos(pitch) * Math.Sin(yaw));
float z = (float)Math.Sin(pitch);
return new Vector(x, y, z);
}
public static void Look(this CBasePlayerPawn pawn, QAngle angle)
{
if (pawn == null || !pawn.IsValid) return;
SnapViewAngles.Value?.Invoke(pawn, angle);
}
public static CBeam? CreateLine(Vector start, Vector end, Color color, uint ownerPlayerIndex = EntityManager.SystemOwnerIndex)
{
return EntityManager.CreateTrackedBeam(ownerPlayerIndex, start, end, color);
}
public static void SetPlayerCollisions(CCSPlayerController? player, bool enable)
{
return;
if (player == null || !player.IsValid) return;
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE || pawn.CBodyComponent == null) return;
var collision = pawn.Collision;
if (collision == null) return;
var collisionGroup = (byte)(enable ? CollisionGroup.COLLISION_GROUP_PLAYER : CollisionGroup.COLLISION_GROUP_DEBRIS);
collision.CollisionGroup = collisionGroup;
collision.CollisionAttribute.CollisionGroup = collisionGroup;
Utilities.SetStateChanged(pawn, "CCollisionProperty", "m_collisionAttribute");
// CollisionRulesChanged(pawn);
}
//public static void CollisionRulesChanged(CBaseEntity? entity)
//{
// if (entity == null || !entity.IsValid || collisionRulesChangedOffset <= 0) return;
// var collisionRulesChanged = new VirtualFunctionVoid<nint>(entity.Handle, collisionRulesChangedOffset);
// collisionRulesChanged.Invoke(entity.Handle);
//}
public static void ApplyScreenColor(CCSPlayerController? player, int r, int g, int b, int a, int duration, int holdTime, int flags = 1)
{
if (player == null || !player.IsValid) return;
using var msg = UserMessage.FromPartialName("Fade");
if (msg == null) return;
int packageColor = (a << 24) | (b << 16) | (g << 8) | r;
msg.SetInt("duration", duration);
msg.SetInt("hold_time", holdTime);
msg.SetInt("flags", flags);
msg.SetInt("color", packageColor);
msg.Send(player);
}
public static void ChangePlayerScale(CCSPlayerController? player, float scale)
{
if (player == null || !player.IsValid) return;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid || playerPawn.CBodyComponent == null || playerPawn.CBodyComponent.SceneNode == null) return;
var skeleton = playerPawn.CBodyComponent.SceneNode.GetSkeletonInstance();
if (scale <= 0 || skeleton == null) return;
skeleton.Scale = scale;
playerPawn.AcceptInput("SetScale", null, null, scale.ToString(CultureInfo.InvariantCulture));
Server.NextWorldUpdate(() => {
if (playerPawn == null || !playerPawn.IsValid) return;
Utilities.SetStateChanged(playerPawn, "CBaseEntity", "m_CBodyComponent");
});
}
public static Vector? GetSpawnPointVector(CCSPlayerController player, bool enemySpawn = false)
{
if (player == null || player.Team is CsTeam.None or CsTeam.Spectator) return null;
CsTeam targetTeam = enemySpawn
? (player.Team == CsTeam.CounterTerrorist ? CsTeam.Terrorist : CsTeam.CounterTerrorist)
: player.Team;
string spawnPointName = targetTeam == CsTeam.CounterTerrorist
? "info_player_counterterrorist"
: "info_player_terrorist";
var spawns = Utilities.FindAllEntitiesByDesignerName<SpawnPoint>(spawnPointName).Where(s => s.IsValid && s.Enabled).ToList();
if (spawns.Count != 0)
{
var randomSpawn = spawns[jRandomSkills.Instance.Random.Next(spawns.Count)];
if (randomSpawn != null && randomSpawn.IsValid && randomSpawn.AbsOrigin != null)
return new Vector(randomSpawn.AbsOrigin.X, randomSpawn.AbsOrigin.Y, randomSpawn.AbsOrigin.Z);
}
return null;
}
public static void CreateHEGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int 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.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 45, teamNum);
}
public static bool TakeHealth(CCSPlayerPawn? pawn, int damage)
{
if (pawn == null || !pawn.IsValid || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE)
return false;
var player = pawn.Controller.Value;
if (player != null && player.IsValid && player.SteamID != 0)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return false;
if (playerInfo.Skill == Skills.Jester && Jester.GetJesterInfo(player.Index)?.Active == true)
return false;
if (playerInfo.Skill == Skills.GodMode && GodMode.HaveHodMode(player.Index))
return false;
}
int newHealth = (int)(pawn.Health - damage);
pawn.Health = newHealth;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
if (pawn.Health <= 0)
{
Server.NextFrame(() =>
{
if (pawn == null || !pawn.IsValid) return;
pawn?.CommitSuicide(false, true);
});
return false;
}
return true;
}
public static void ResetPrintHTML(CCSPlayerController? player)
{
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
playerInfo.PrintHTML = null;
}
public static CTriggerMultiple? CreateTrigger(string name, float radius, Vector pos, uint ownerPlayerIndex = EntityManager.SystemOwnerIndex)
{
return EntityManager.CreateTrackedTrigger(ownerPlayerIndex, name, radius, pos);
}
public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null, INetworkGameServer? networkGameServer = null)
{
if (!Config.LoadedConfig.EnableFullForceUpdate) return;
if (player == null || !player.IsValid) return;
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null) return;
QAngle lastAngle = new(pawn.V_angle.X, pawn.V_angle.Y, pawn.V_angle.Z);
networkGameServer ??= new INetworkServerService().GetIGameServer();
var client = networkGameServer.GetClientBySlot(player.Slot);
if (client == null) return;
client.ForceFullUpdate();
// 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;
if (batchList != null)
{
batchList.Add((playerIndex, lastAngle));
return;
}
jRandomSkills.Instance.AddTickTimer(3, () =>
{
var target = Utilities.GetPlayerFromIndex((int)playerIndex);
if (target == null || !target.IsValid) return;
var targetPawn = target.PlayerPawn?.Value;
if (targetPawn == null || !targetPawn.IsValid || targetPawn.AbsOrigin == null) return;
targetPawn.Look(lastAngle);
});
}
private static int lastForceFullUpdateAll = int.MinValue;
public static void ForceFullUpdateToAll()
{
if (!Config.LoadedConfig.EnableFullForceUpdate) return;
int tickCount = Server.TickCount;
if (tickCount == lastForceFullUpdateAll) return;
lastForceFullUpdateAll = tickCount;
var playersToRestore = new List<(uint PlayerIndex, QAngle LastAngle)>();
INetworkGameServer networkGameServer = new INetworkServerService().GetIGameServer();
foreach (var player in Utilities.GetPlayers())
ForceFullUpdate(player, playersToRestore, networkGameServer);
if (playersToRestore.Count <= 0) return;
jRandomSkills.Instance.AddTickTimer(3, () =>
{
foreach (var item in playersToRestore)
{
var target = Utilities.GetPlayerFromIndex((int)item.PlayerIndex);
if (target == null || !target.IsValid) continue;
var targetPawn = target.PlayerPawn?.Value;
if (targetPawn == null || !targetPawn.IsValid || targetPawn.AbsOrigin == null) continue;
targetPawn.Look(item.LastAngle);
}
});
}
public static bool SetHealth(CCSPlayerPawn? pawn, int newHealth, int? maxHealth = null)
{
if (pawn == null || !pawn.IsValid)
return false;
maxHealth ??= pawn.MaxHealth;
if (pawn.Health == maxHealth)
return false;
pawn.Health = Math.Min(newHealth, (int)maxHealth);
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
pawn.MaxHealth = (int)maxHealth;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iMaxHealth");
return true;
}
public static bool AddHealth(CCSPlayerPawn? pawn, int extraHealth, int? maxHealth = null)
{
if (pawn == null || !pawn.IsValid || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE || pawn.Health <= 0)
return false;
maxHealth ??= pawn.MaxHealth;
if (pawn.Health == maxHealth)
return false;
int newHealth = (int)(pawn.Health + extraHealth);
pawn.Health = Math.Min(newHealth, (int)maxHealth);
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
pawn.MaxHealth = (int)maxHealth;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iMaxHealth");
return true;
}
public static void RestoreHealth(CCSPlayerController? player)
{
if (player == null || !player.IsValid || player.PlayerPawn == null)
return;
CBasePlayerPawn? pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE)
return;
var p = PlayerManager.GetPlayerFromEvent(player);
if (p == null || !p.IsValid)
return;
pawn.Health = (int)p.PawnHealth;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
}
public static void SetPlayerInvisibility(CCSPlayerController player, float percentInvisibility)
{
if (player == null || !player.IsValid || player.PlayerPawn == null)
return;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null)
{
var color = Color.FromArgb(Math.Max(255 - (int)(255 * percentInvisibility), 0), 255, 255, 255);
playerPawn.Render = color;
Utilities.SetStateChanged(playerPawn, "CBaseModelEntity", "m_clrRender");
}
}
public static string GetDesignerName(CBasePlayerWeapon? weapon)
{
if (weapon == null || !weapon.IsValid) return string.Empty;
string designerName = weapon.DesignerName;
ushort index = weapon.AttributeManager.Item.ItemDefinitionIndex;
designerName = (designerName, index) switch
{
var (name, _) when name.Contains("bayonet") => "weapon_knife",
("weapon_m4a1", 60) => "weapon_m4a1_silencer",
("weapon_hkp2000", 61) => "weapon_usp_silencer",
("weapon_deagle", 64) => "weapon_revolver",
_ => designerName
};
return designerName;
}
private static IWasdMenuManager? GetMenuManager()
{
if (jRandomSkills.Instance.MenuManager == null)
jRandomSkills.Instance.MenuManager = new WasdManager();
return jRandomSkills.Instance.MenuManager;
}
public static void CloseMenu(CCSPlayerController? player)
{
var manager = GetMenuManager();
if (manager == null) return;
manager.CloseMenu(player);
}
public static bool HasMenu(CCSPlayerController? player)
{
var manager = GetMenuManager();
if (manager == null) return false;
return manager.HasMenu(player);
}
private static string GetInvisibleSignature(string id)
{
if (string.IsNullOrEmpty(id)) return "";
var sb = new System.Text.StringBuilder();
foreach (char c in id)
for (int i = 0; i < 8; i++)
sb.Append(((c >> i) & 1) == 1 ? "\u200B" : "\u200C");
return sb.ToString();
}
public static void UpdateMenu(CCSPlayerController? player, ConcurrentBag<(string, string)> items)
{
if (player == null) return;
var manager = GetMenuManager();
if (manager == null) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
bool isIlliterate = Illiterate.CheckIlliterateSkill(player);
Dictionary<string, Action<CCSPlayerController, IWasdMenuOption>> list = [];
foreach (var item in items)
{
string encodedText = isIlliterate
? System.Net.WebUtility.HtmlEncode(Illiterate.GetRandomText(item.Item1)!)
: System.Net.WebUtility.HtmlEncode(item.Item1);
string uniqueKey = GetInvisibleSignature(item.Item2) + $"\u202A{encodedText}\u202C";
list.TryAdd(uniqueKey, (p, option) =>
{
jRandomSkills.Instance.SkillAction(playerInfo.Skill.ToString(), "TypeSkill", [p, new[] { item.Item2 }]);
manager.CloseMenu(p);
});
}
manager.UpdateActiveMenu(player, list);
}
public static void CreateMenu(CCSPlayerController? player, ConcurrentBag<(string, string)> enemies, (string, string, bool)? lastElement = null)
{
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null || !playerInfo.DisplayHUD) return;
if (player.IsBot)
{
var pool = new List<string>();
foreach (var enemy in enemies)
if (!string.IsNullOrEmpty(enemy.Item2))
pool.Add(enemy.Item2);
if (lastElement != null && !string.IsNullOrEmpty(lastElement.Value.Item2))
pool.Add(lastElement.Value.Item2);
if (pool.Count > 0)
{
string randomTarget = pool[Random.Shared.Next(pool.Count)];
jRandomSkills.Instance.SkillAction(playerInfo.Skill.ToString(), "TypeSkill", [player, new[] { randomTarget }]);
}
return;
}
var skillData = SkillData.Skills.FirstOrDefault(s => s.Skill == playerInfo.Skill);
if (skillData == null) return;
var manager = GetMenuManager();
if (manager == null) return;
var config = Config.LoadedConfig.HtmlHudCustomisation;
var your_skill = player.GetTranslation("your_skill");
var emptySymbol = $"<font class='fontSize-{(string.IsNullOrEmpty(your_skill) ? "l" : "ml")}'> </font>";
string infoLine = string.IsNullOrEmpty(your_skill)
? ""
: $"<font class='fontWeight-Bold fontSize-{config.HeaderLineSize}' color='{config.HeaderLineColor}'>\u202A{your_skill}:\u202C</font><br>";
string skillLine = Illiterate.CheckIlliterateSkill(player)
? $"<font class='fontWeight-Bold fontSize-{config.SkillLineSize}'>\u202A{Illiterate.GetRandomText(player.GetSkillName(skillData.Skill))}\u202C</font><br>"
: $"<font class='fontWeight-Bold fontSize-{config.SkillLineSize}' color='{skillData.Color}'>\u202A{player.GetSkillName(skillData.Skill)}\u202C</font><br>";
var skill_select_info = player.GetTranslation($"{playerInfo.Skill.ToString().ToLowerInvariant()}_select_info");
string remainingLine = string.IsNullOrWhiteSpace(skill_select_info)
? ""
: $"<font class='fontSize-{config.WSADMenuSelectInfoLineSize}' color='{config.WSADMenuSelectInfoLineColor}'>{skill_select_info}</font><br>";
var hudContent = infoLine + skillLine + remainingLine;
string controllsLine =
$"{emptySymbol}<font class='fontSize-{config.WSADMenuControllsLineSize}' color='{config.WSADMenuControllsLineColor1}'>{player.GetTranslation($"menu_controlls_scroll")}</font>"
+ $"<font class='fontSize-{config.WSADMenuControllsLineSize}' color='{config.WSADMenuControllsLineColor2}'>{player.GetTranslation($"menu_controlls_padding")}</font>"
+ $"<font class='fontSize-{config.WSADMenuControllsLineSize}' color='{config.WSADMenuControllsLineColor3}'>{player.GetTranslation($"menu_controlls_select")}</font>{emptySymbol}";
string itemText = $"<font class='fontSize-{config.WSADMenuItemLineSize}' color='{config.WSADMenuItemLineColor}'>{{0}}</font><br>";
string itemHoverText = $"<font class='fontSize-{config.WSADMenuItemLineSize}'><font color='purple'>[ </font><font color='{config.WSADMenuItemHoverLineColor}'>{{0}}</font><font color='purple'> ]</font></font><br>";
bool isIlliterate = Illiterate.CheckIlliterateSkill(player);
IWasdMenu menu = manager.CreateMenu(hudContent, itemText, itemHoverText, controllsLine);
foreach (var enemy in enemies)
{
string encodedEnemyName = isIlliterate
? System.Net.WebUtility.HtmlEncode(Illiterate.GetRandomText(enemy.Item1)!)
: System.Net.WebUtility.HtmlEncode(enemy.Item1);
string uniqueKey = GetInvisibleSignature(enemy.Item2) + $"\u202A{encodedEnemyName}\u202C";
menu.Add(uniqueKey, (p, option) =>
{
jRandomSkills.Instance.SkillAction(playerInfo.Skill.ToString(), "TypeSkill", [p, new[] { enemy.Item2 }]);
manager.CloseMenu(p);
});
}
if (lastElement != null)
{
string encodedLastElement = isIlliterate
? System.Net.WebUtility.HtmlEncode(Illiterate.GetRandomText(lastElement.Value.Item1)!)
: System.Net.WebUtility.HtmlEncode(lastElement.Value.Item1);
menu.Add($"\u202A{encodedLastElement}\u202C", (p, option) =>
{
jRandomSkills.Instance.SkillAction(playerInfo.Skill.ToString(), "TypeSkill", [p, new[] { lastElement.Value.Item2 }]);
if (lastElement.Value.Item3)
manager.CloseMenu(p);
});
}
manager.OpenMainMenu(player, menu);
}
public static void SetTeamScores(short ctScore, short tScore, RoundEndReason roundEndReason)
{
if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return;
UpdateServerTeamScores(ctScore, tScore);
TerminateRoundFunc.Value?.Invoke(jRandomSkills.Instance.GameRules.Handle, 5f, roundEndReason, 0, 0);
}
public static void TerminateRound(CsTeam winnerTeam)
{
if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return;
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager");
var ctTeam = teams.FirstOrDefault(t => t.IsValid && (CsTeam)t.TeamNum == CsTeam.CounterTerrorist);
var tTeams = teams.FirstOrDefault(t => t.IsValid && (CsTeam)t.TeamNum == CsTeam.Terrorist);
if (ctTeam == null || tTeams == null) return;
short ctScore = (short)(winnerTeam == CsTeam.CounterTerrorist ? ctTeam.Score + 1 : ctTeam.Score);
short tScore = (short)(winnerTeam == CsTeam.Terrorist ? tTeams.Score + 1 : tTeams.Score);
UpdateServerTeamScores(ctScore, tScore);
jRandomSkills.Instance.GameRules?.TerminateRound(5f, winnerTeam == CsTeam.CounterTerrorist ? RoundEndReason.BombDefused : RoundEndReason.TargetBombed);
// TerminateRoundFunc.Invoke(jRandomSkills.Instance.GameRules.Handle, 5f, winnerTeam == CsTeam.CounterTerrorist ? RoundEndReason.BombDefused : RoundEndReason.TargetBombed, 0, 0);
}
private static void UpdateServerTeamScores(short ctScore, short tScore)
{
if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return;
int totalRoundsPlayed = ctScore + tScore;
int maxRounds = ConVar.Find("mp_maxrounds")?.GetPrimitiveValue<int>() ?? 24;
int halfRounds = maxRounds / 2;
int overtimeMaxRounds = ConVar.Find("mp_overtime_maxrounds")?.GetPrimitiveValue<int>() ?? 6;
int overtimeLimit = ConVar.Find("mp_overtime_limit")?.GetPrimitiveValue<int>() ?? 1;
var gameRulesProxy = jRandomSkills.Instance.GameRules;
gameRulesProxy.TotalRoundsPlayed = totalRoundsPlayed;
gameRulesProxy.ITotalRoundsPlayed = totalRoundsPlayed;
gameRulesProxy.RoundsPlayedThisPhase = totalRoundsPlayed;
gameRulesProxy.TeamIntroPeriod = false;
if (gameRulesProxy.GamePhase == 1 && totalRoundsPlayed < halfRounds)
{
gameRulesProxy.GamePhase = 0;
gameRulesProxy.SwapTeamsOnRestart = true;
gameRulesProxy.SwitchingTeamsAtRoundReset = true;
gameRulesProxy.RoundsPlayedThisPhase = 0;
gameRulesProxy.TeamIntroPeriod = true;
}
if (totalRoundsPlayed < halfRounds)
gameRulesProxy.GamePhase = 0;
else if (gameRulesProxy.GamePhase == 0)
{
gameRulesProxy.GamePhase = 1;
gameRulesProxy.SwapTeamsOnRestart = true;
gameRulesProxy.SwitchingTeamsAtRoundReset = true;
gameRulesProxy.RoundsPlayedThisPhase = 0;
gameRulesProxy.TeamIntroPeriod = true;
}
var structOffset = jRandomSkills.Instance.GameRules.Handle + Schema.GetSchemaOffset("CCSGameRules", "m_bMapHasBombZone") + 0x02;
var matchStruct = Marshal.PtrToStructure<MCCSMatch>(structOffset);
matchStruct.m_totalScore = (short)totalRoundsPlayed;
matchStruct.m_actualRoundsPlayed = (short)totalRoundsPlayed;
gameRulesProxy.MatchInfoDecidedTime = Server.CurrentTime;
matchStruct.m_ctScoreTotal = ctScore;
gameRulesProxy.AccountCT = ctScore;
matchStruct.m_terroristScoreTotal = tScore;
gameRulesProxy.AccountTerrorist = tScore;
if (gameRulesProxy.GamePhase == 0)
{
matchStruct.m_ctScoreFirstHalf = ctScore;
matchStruct.m_terroristScoreFirstHalf = tScore;
}
else
{
matchStruct.m_ctScoreSecondHalf = ctScore;
matchStruct.m_terroristScoreSecondHalf = tScore;
}
if (totalRoundsPlayed >= maxRounds)
{
if (gameRulesProxy.OvertimePlaying == 0)
{
gameRulesProxy.OvertimePlaying = 1;
gameRulesProxy.SwapTeamsOnRestart = true;
gameRulesProxy.SwitchingTeamsAtRoundReset = true;
}
else
{
int roundsInOvertime = totalRoundsPlayed - maxRounds;
if (roundsInOvertime % overtimeMaxRounds == 0)
{
int currentOvertime = roundsInOvertime / overtimeMaxRounds;
if ( currentOvertime < overtimeLimit)
{
gameRulesProxy.SwapTeamsOnRestart = true;
gameRulesProxy.SwitchingTeamsAtRoundReset = true;
}
}
}
}
gameRulesProxy.OvertimePlaying = 0;
Marshal.StructureToPtr(matchStruct, structOffset, true);
UpdateClientTeamScores(matchStruct);
}
private static void UpdateClientTeamScores(MCCSMatch match)
{
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager");
var ctTeam = teams.FirstOrDefault(t => t.IsValid && (CsTeam)t.TeamNum == CsTeam.CounterTerrorist);
var tTeams = teams.FirstOrDefault(t => t.IsValid && (CsTeam)t.TeamNum == CsTeam.Terrorist);
if (ctTeam != null && tTeams != null)
{
ctTeam.Score = match.m_ctScoreTotal;
ctTeam.ScoreFirstHalf = match.m_ctScoreFirstHalf;
ctTeam.ScoreSecondHalf = match.m_ctScoreSecondHalf;
ctTeam.ScoreOvertime = match.m_ctScoreOvertime;
Utilities.SetStateChanged(ctTeam, "CTeam", "m_iScore");
Utilities.SetStateChanged(ctTeam, "CCSTeam", "m_scoreFirstHalf");
Utilities.SetStateChanged(ctTeam, "CCSTeam", "m_scoreSecondHalf");
Utilities.SetStateChanged(ctTeam, "CCSTeam", "m_scoreOvertime");
tTeams.Score = match.m_terroristScoreTotal;
tTeams.ScoreFirstHalf = match.m_terroristScoreFirstHalf;
tTeams.ScoreSecondHalf = match.m_terroristScoreSecondHalf;
tTeams.ScoreOvertime = match.m_terroristScoreOvertime;
Utilities.SetStateChanged(tTeams, "CTeam", "m_iScore");
Utilities.SetStateChanged(tTeams, "CCSTeam", "m_scoreFirstHalf");
Utilities.SetStateChanged(tTeams, "CCSTeam", "m_scoreSecondHalf");
Utilities.SetStateChanged(tTeams, "CCSTeam", "m_scoreOvertime");
}
}
}
[StructLayout(LayoutKind.Sequential)]
public struct MCCSMatch
{
public short m_totalScore;
public short m_actualRoundsPlayed;
public short m_nOvertimePlaying;
public short m_ctScoreFirstHalf;
public short m_ctScoreSecondHalf;
public short m_ctScoreOvertime;
public short m_ctScoreTotal;
public short m_terroristScoreFirstHalf;
public short m_terroristScoreSecondHalf;
public short m_terroristScoreOvertime;
public short m_terroristScoreTotal;
public short unknown;
public int m_phase;
}
}