commit
559e7be23f
28 changed files with 469 additions and 400 deletions
|
|
@ -15,7 +15,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" />
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.371" />
|
||||
<PackageReference Include="MaxMind.Db" Version="5.1.0">
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
"signatures": {
|
||||
"library": "server",
|
||||
"windows": "48 8B C4 48 89 58 ? 48 89 68 ? 48 89 70 ? 57 41 56 41 57 48 81 EC ? ? ? ? 48 8B B4 24 ? ? ? ? 4D 8B F8",
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 45 89 CF 41 56 49 89 FE"
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 45 89 CE 41 55 4D 89 C5 41 54 53 48 83 EC ? 48 89 55 ? 48 89 F2 48 89 FE"
|
||||
}
|
||||
},
|
||||
"HEGrenadeProjectile_CreateFunc": {
|
||||
"signatures": {
|
||||
"library": "server",
|
||||
"windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B AC 24 ? ? ? ? 49 8B F8",
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 D7"
|
||||
"windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B 6C 24 ? 49 8B F8 4C 8B C2 0F 29 74 24 ? 48 8B D1 48 8B D9 48 8D 0D ? ? ? ? 4C 8B CD E8 ? ? ? ? F3 0F 10 0D ? ? ? ? 48 8B C8 48 8B F0 E8 ? ? ? ? 48 8B D7 48 8B CE",
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 49 89 D6 48 89 F2 48 89 FE 41 55 48 8D 3D ? ? ? ? 4D 89 C5 41 54 45 89 CC 53"
|
||||
}
|
||||
},
|
||||
"Shoot_Secondary": {
|
||||
|
|
@ -24,25 +24,7 @@
|
|||
"signatures": {
|
||||
"library": "server",
|
||||
"windows": "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 48 8D 6C 24 ? 48 81 EC ? ? ? ? 48 8B DA 48 8B F1 48 8D 55",
|
||||
"linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC"
|
||||
}
|
||||
},
|
||||
"CServerSideClient_m_nDeltaTick": {
|
||||
"offsets": {
|
||||
"windows": 348,
|
||||
"linux": 348
|
||||
}
|
||||
},
|
||||
"INetworkGameServer_Slots": {
|
||||
"offsets": {
|
||||
"windows": 592,
|
||||
"linux": 592
|
||||
}
|
||||
},
|
||||
"INetworkServerService_GetIGameServer": {
|
||||
"offsets": {
|
||||
"windows": 23,
|
||||
"linux": 24
|
||||
"linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC ? ? ? ? E8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ namespace src
|
|||
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.b6";
|
||||
public override string ModuleVersion => "1.2.2.b7";
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
|
|
@ -56,6 +56,13 @@ namespace src
|
|||
});
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
src.player.PerfLog.Info("===== PLUGIN UNLOAD (clean shutdown/reload) =====");
|
||||
Debug.WriteToDebug("===== PLUGIN UNLOAD (clean shutdown/reload) =====");
|
||||
base.Unload(hotReload);
|
||||
}
|
||||
|
||||
internal void AddToManifest(string prop)
|
||||
{
|
||||
if (!ManifestResources.Contains(prop))
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ namespace src.player
|
|||
{
|
||||
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.
|
||||
// First write creates the file so an active PerfMode is immediately visible.
|
||||
if (!_headerWritten)
|
||||
{
|
||||
_headerWritten = true;
|
||||
|
|
@ -31,6 +30,12 @@ namespace src.player
|
|||
return Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
public static void Info(string message)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Write(message);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -144,7 +144,15 @@ namespace src.player
|
|||
foreach (var p in Instance.SkillPlayer)
|
||||
{
|
||||
if (p.IsDrawing || !seen.Add(p.Skill)) continue;
|
||||
Instance.SkillAction(p.Skill.ToString(), methodName, args);
|
||||
try
|
||||
{
|
||||
Instance.SkillAction(p.Skill.ToString(), methodName, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// One skill's failure must not break the dispatch chain.
|
||||
Server.PrintToConsole($"[jRandomSkills] {p.Skill}.{methodName} failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,7 +376,7 @@ namespace src.player
|
|||
if (playerInfo == null) return HookResult.Continue;
|
||||
|
||||
CCSWeaponBaseVData vdata = VirtualFunctions.GetCSWeaponDataFromKeyFunc.Invoke(-1, econItem.ItemDefinitionIndex.ToString());
|
||||
if (vdata == null) return HookResult.Continue;
|
||||
if (vdata == null || vdata.Handle == IntPtr.Zero) return HookResult.Continue;
|
||||
|
||||
var activeSkills = Instance.SkillPlayer
|
||||
.Where(p => !p.IsDrawing)
|
||||
|
|
@ -507,6 +515,8 @@ namespace src.player
|
|||
var player = PlayerManager.GetPlayerEvent(@event.Userid);
|
||||
if (player == null || !player.IsValid) return HookResult.Continue;
|
||||
|
||||
Localization.PreResolveLanguage(player);
|
||||
|
||||
string welcomeMsg = player.GetTranslation("welcome_message", "welcome");
|
||||
foreach (string line in welcomeMsg.Split("\n"))
|
||||
player.PrintToChat($" {ChatColors.Green}" + line.Replace("{PLAYER}", $" {ChatColors.Red}\u202A{player.PlayerName}\u202C{ChatColors.Green}", StringComparison.OrdinalIgnoreCase)
|
||||
|
|
@ -613,6 +623,12 @@ namespace src.player
|
|||
|
||||
setSkillTimer?.Kill();
|
||||
|
||||
if (isWarmup)
|
||||
{
|
||||
setSkillTimer = Instance?.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
float timeToDraw = (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0) + Math.Max(freezetime - Config.LoadedConfig.SkillTimeBeforeStart, 0) + .3f;
|
||||
setSkillTimer = Instance?.AddTimer(timeToDraw, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
return HookResult.Continue;
|
||||
|
|
@ -630,6 +646,9 @@ namespace src.player
|
|||
{
|
||||
lock (setLock)
|
||||
{
|
||||
// Re-register CheckTransmit so the dying-entity filter covers the kills below.
|
||||
EnableTransmit();
|
||||
|
||||
Fortnite.skillInThisRound = false;
|
||||
EntityManager.DestroyAllTracked();
|
||||
|
||||
|
|
@ -685,10 +704,15 @@ namespace src.player
|
|||
Instance.RemoveListener<CheckTransmit>(CheckTransmit);
|
||||
|
||||
Fortnite.skillInThisRound = false;
|
||||
|
||||
EntityManager.SuppressKills = true;
|
||||
EntityManager.DestroyAllTracked();
|
||||
foreach (var skill in SkillData.Skills)
|
||||
Instance.SkillAction(skill.Skill.ToString(), "NewRound");
|
||||
EntityManager.SuppressKills = false;
|
||||
|
||||
ActiveSkillsThisRound.Clear();
|
||||
nextRoundPicks.Clear();
|
||||
|
||||
playersSkills.Clear();
|
||||
staticSkills.Clear();
|
||||
|
|
@ -740,6 +764,10 @@ namespace src.player
|
|||
}
|
||||
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
|
||||
// Before the optional disable below, so the "don't repeat the current skill"
|
||||
// exclusion still sees this round's skills.
|
||||
Instance.AddTimer(.6f, PrecomputeNextRoundSkills, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
|
||||
if (Config.LoadedConfig.DisableSkillsOnRoundEnd)
|
||||
{
|
||||
isTransmitRegistered = false;
|
||||
|
|
@ -870,6 +898,98 @@ namespace src.player
|
|||
PerfLog.End("SetSkill total", perfStart, 2.0);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<uint, jSkill_SkillInfo> nextRoundPicks = [];
|
||||
|
||||
private static jSkill_SkillInfo PickSkillForPlayer(CCSPlayerController player, jSkill_PlayerInfo skillPlayer, List<CCSPlayerController> validPlayers, Dictionary<Skills, int> assignmentCounts, Config.GameModes gameMode)
|
||||
{
|
||||
List<jSkill_SkillInfo> skillList = [.. SkillData.Skills];
|
||||
skillList.RemoveAll(s => s?.Skill == Skills.None);
|
||||
if (!player.IsBot)
|
||||
skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue<string>(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(player, SkillsInfo.GetValue<string>(s.Skill, "requiredPermission")));
|
||||
|
||||
if (gameMode != Config.GameModes.FullRandom)
|
||||
skillList.RemoveAll(s => s?.Skill == skillPlayer?.Skill || s?.Skill == skillPlayer?.SpecialSkill);
|
||||
|
||||
if (validPlayers.Count(p => p.Team == player.Team) == 1)
|
||||
{
|
||||
SkillsInfo.DefaultSkillInfo[] skillsNeedsTeammates = [.. SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates)];
|
||||
skillList.RemoveAll(s => skillsNeedsTeammates.Any(s2 => s2.Name == s.Skill.ToString()));
|
||||
}
|
||||
|
||||
if (player.Team == CsTeam.Terrorist)
|
||||
skillList.RemoveAll(s => counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString()));
|
||||
else
|
||||
skillList.RemoveAll(s => terroristSkills.Any(s2 => s2.Name == s.Skill.ToString()));
|
||||
|
||||
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? skills))
|
||||
{
|
||||
skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill));
|
||||
if (skillList.Count == 0) skills.Clear();
|
||||
}
|
||||
|
||||
var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode);
|
||||
|
||||
if (gameMode == Config.GameModes.NoRepeat)
|
||||
{
|
||||
if (playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? value))
|
||||
value.Add(randomSkill);
|
||||
else
|
||||
playersSkills.TryAdd(player.Index, [randomSkill]);
|
||||
}
|
||||
|
||||
return randomSkill;
|
||||
}
|
||||
|
||||
private static bool IsPickStillValid(jSkill_SkillInfo pick, CCSPlayerController player, List<CCSPlayerController> validPlayers, Dictionary<Skills, int> assignmentCounts)
|
||||
{
|
||||
if (pick.Skill == Skills.None) return true;
|
||||
if (!SkillData.Skills.Any(s => s.Skill == pick.Skill)) return false;
|
||||
|
||||
string name = pick.Skill.ToString();
|
||||
if (player.Team == CsTeam.Terrorist && counterterroristSkills.Any(s => s.Name == name)) return false;
|
||||
if (player.Team == CsTeam.CounterTerrorist && terroristSkills.Any(s => s.Name == name)) return false;
|
||||
|
||||
var def = SkillsInfo.LoadedConfig.FirstOrDefault(d => d.Name == name);
|
||||
if (def == null) return false;
|
||||
if (def.NeedsTeammates && validPlayers.Count(p => p.Team == player.Team) == 1) return false;
|
||||
if (def.MaxPerServer >= 0 && assignmentCounts.TryGetValue(pick.Skill, out var c) && c >= def.MaxPerServer) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Runs at round end so the expensive skill selection is off the round-start hot path;
|
||||
// SetSkillCore then only applies the picks.
|
||||
private static void PrecomputeNextRoundSkills()
|
||||
{
|
||||
long perfStart = PerfLog.Start();
|
||||
lock (setLock)
|
||||
{
|
||||
nextRoundPicks.Clear();
|
||||
|
||||
var gameMode = (Config.GameModes)Config.LoadedConfig.GameMode;
|
||||
if (gameMode is not (Config.GameModes.Normal or Config.GameModes.FullRandom or Config.GameModes.NoRepeat)) return;
|
||||
if (Instance?.GameRules == null || Instance.GameRules.WarmupPeriod == true) return;
|
||||
|
||||
var validPlayers = Utilities.GetPlayers()
|
||||
.Where(p => p != null && p.IsValid && !p.IsHLTV)
|
||||
.Where(p => { try { return p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist; } catch { return false; } }).ToList();
|
||||
|
||||
Dictionary<Skills, int> assignmentCounts = [];
|
||||
foreach (var player in validPlayers)
|
||||
{
|
||||
var skillPlayer = PlayerManager.GetPlayerByIndex(player.Index);
|
||||
if (skillPlayer == null) continue;
|
||||
|
||||
var pick = PickSkillForPlayer(player, skillPlayer, validPlayers, assignmentCounts, gameMode);
|
||||
nextRoundPicks[player.Index] = pick;
|
||||
|
||||
if (pick.Skill != Skills.None)
|
||||
assignmentCounts[pick.Skill] = assignmentCounts.TryGetValue(pick.Skill, out var c) ? c + 1 : 1;
|
||||
}
|
||||
}
|
||||
PerfLog.End("PrecomputeSkills total", perfStart, 2.0);
|
||||
}
|
||||
|
||||
private static void SetSkillCore()
|
||||
{
|
||||
setSkillTimer = null;
|
||||
|
|
@ -877,11 +997,11 @@ namespace src.player
|
|||
{
|
||||
if (Instance == null) return;
|
||||
|
||||
// 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.
|
||||
// GameRules null = not ready; keep polling so skills land right after warmup ends.
|
||||
if (Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true)
|
||||
{
|
||||
setSkillTimer?.Kill();
|
||||
setSkillTimer = Instance.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -940,40 +1060,12 @@ namespace src.player
|
|||
Config.GameModes gameMode = (Config.GameModes)Config.LoadedConfig.GameMode;
|
||||
if (gameMode == Config.GameModes.Normal || gameMode == Config.GameModes.FullRandom || gameMode == Config.GameModes.NoRepeat)
|
||||
{
|
||||
List<jSkill_SkillInfo> skillList = [.. SkillData.Skills];
|
||||
skillList.RemoveAll(s => s?.Skill == Skills.None);
|
||||
if (!player.IsBot)
|
||||
skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue<string>(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(player, SkillsInfo.GetValue<string>(s.Skill, "requiredPermission")));
|
||||
|
||||
if (gameMode != Config.GameModes.FullRandom)
|
||||
skillList.RemoveAll(s => s?.Skill == skillPlayer?.Skill || s?.Skill == skillPlayer?.SpecialSkill);
|
||||
|
||||
if (validPlayers.Count(p => p.Team == player.Team) == 1)
|
||||
{
|
||||
SkillsInfo.DefaultSkillInfo[] skillsNeedsTeammates = [.. SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates)];
|
||||
skillList.RemoveAll(s => skillsNeedsTeammates.Any(s2 => s2.Name == s.Skill.ToString()));
|
||||
}
|
||||
|
||||
if (player.Team == CsTeam.Terrorist)
|
||||
skillList.RemoveAll(s => counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString()));
|
||||
// Prefer the pick made at the end of the previous round; re-pick only when
|
||||
// it no longer fits (team change, missing player, max reached).
|
||||
if (nextRoundPicks.TryGetValue(player.Index, out var pre) && IsPickStillValid(pre, player, validPlayers, assignmentCounts))
|
||||
randomSkill = pre;
|
||||
else
|
||||
skillList.RemoveAll(s => terroristSkills.Any(s2 => s2.Name == s.Skill.ToString()));
|
||||
|
||||
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? skills))
|
||||
{
|
||||
skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill));
|
||||
if (skillList.Count == 0) skills.Clear();
|
||||
}
|
||||
|
||||
randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode);
|
||||
|
||||
if (gameMode == Config.GameModes.NoRepeat)
|
||||
{
|
||||
if (playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? value))
|
||||
value.Add(randomSkill);
|
||||
else
|
||||
playersSkills.TryAdd(player.Index, [randomSkill]);
|
||||
}
|
||||
randomSkill = PickSkillForPlayer(player, skillPlayer, validPlayers, assignmentCounts, gameMode);
|
||||
}
|
||||
else if (gameMode == Config.GameModes.TeamSkills)
|
||||
randomSkill = player.Team == CsTeam.Terrorist ? tSkill : ctSkill;
|
||||
|
|
@ -1057,6 +1149,8 @@ namespace src.player
|
|||
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
}
|
||||
}
|
||||
|
||||
nextRoundPicks.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1178,6 +1272,19 @@ namespace src.player
|
|||
long perfStart = PerfLog.Start();
|
||||
lock (setLock)
|
||||
{
|
||||
// Keep dying entities out of snapshots until the engine processes the kill.
|
||||
var dying = EntityManager.GetRecentlyDestroyedSnapshot();
|
||||
if (dying.Count > 0)
|
||||
{
|
||||
foreach (var (info, player) in infoList)
|
||||
{
|
||||
if (player == null || !player.IsValid) continue;
|
||||
foreach (var entityIndex in dying)
|
||||
if (info.TransmitEntities.Contains(entityIndex))
|
||||
info.TransmitEntities.Remove(entityIndex);
|
||||
}
|
||||
}
|
||||
|
||||
DispatchToActiveSkills("CheckTransmit", infoList);
|
||||
}
|
||||
PerfLog.Sample("CheckTransmit", perfStart);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ namespace src.player
|
|||
UpdateGameRules();
|
||||
if (Server.TickCount % 2 != 0) return;
|
||||
|
||||
if (PerfLog.Enabled && Server.TickCount % 1920 == 0)
|
||||
{
|
||||
int server = Utilities.GetAllEntities().Count(e => e != null && e.IsValid);
|
||||
var (tracked, owners) = EntityManager.GetStatistics();
|
||||
PerfLog.Info($"ENTITIES server={server} tracked={tracked} owners={owners}");
|
||||
}
|
||||
|
||||
long perfStart = PerfLog.Start();
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
{
|
||||
|
|
@ -39,6 +46,8 @@ namespace src.player
|
|||
|
||||
private static void OnMapEnd()
|
||||
{
|
||||
PerfLog.Info("===== MAP END (clean map change) =====");
|
||||
Debug.WriteToDebug("===== MAP END (clean map change) =====");
|
||||
BotManager.Stop();
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +72,10 @@ namespace src.player
|
|||
{
|
||||
if (player == null || !player.IsValid || player.IsBot) return;
|
||||
|
||||
// No skill HUD during warmup or after the match ended.
|
||||
var gameRules = Instance?.GameRules;
|
||||
if (gameRules == null || gameRules.WarmupPeriod == true || gameRules.GamePhase >= 5) return;
|
||||
|
||||
var skillPlayer = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index);
|
||||
if (skillPlayer == null || !skillPlayer.DisplayHUD) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ namespace src.player.skills
|
|||
SkillUtils.SetPlayerInvisibility(player, 0);
|
||||
}
|
||||
|
||||
// CheckTransmit hides the model but the radar blip comes from spotted state, so clear it every tick.
|
||||
if (invisiblePlayers.ContainsKey(player.Index))
|
||||
ClearSpottedState(player);
|
||||
|
||||
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
|
||||
if (playerInfo?.Skill != skillName) continue;
|
||||
|
||||
|
|
@ -100,14 +104,6 @@ namespace src.player.skills
|
|||
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
|
||||
|
||||
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
|
||||
bool isObservingC4Camouflage = false;
|
||||
|
||||
if (targetHandle != nint.Zero)
|
||||
{
|
||||
var target = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == targetHandle);
|
||||
var targetInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(target)?.Index);
|
||||
if (targetInfo?.Skill == skillName) isObservingC4Camouflage = true;
|
||||
}
|
||||
|
||||
foreach (var playerIndex in invisiblePlayers.Keys)
|
||||
{
|
||||
|
|
@ -118,26 +114,27 @@ namespace src.player.skills
|
|||
if (player.Team == playerController.Team)
|
||||
continue;
|
||||
|
||||
if (!isObservingC4Camouflage)
|
||||
{
|
||||
var playerPawn = playerController.PlayerPawn.Value;
|
||||
if (playerPawn == null || !playerPawn.IsValid) continue;
|
||||
var playerPawn = playerController.PlayerPawn.Value;
|
||||
if (playerPawn == null || !playerPawn.IsValid) continue;
|
||||
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
|
||||
if (entity == null || !entity.IsValid) continue;
|
||||
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
|
||||
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
|
||||
continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
|
||||
if (entity == null || !entity.IsValid) continue;
|
||||
|
||||
var bombIndex = GetBombIndex();
|
||||
if (bombIndex == null) continue;
|
||||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
var bombIndex = GetBombIndex();
|
||||
if (bombIndex == null) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
}
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,8 +159,6 @@ namespace src.player.skills
|
|||
|
||||
invisiblePlayers.TryAdd(player.Index, 0);
|
||||
SkillUtils.SetPlayerInvisibility(player, .5f);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
|
|
@ -171,8 +166,6 @@ namespace src.player.skills
|
|||
invisiblePlayers.TryRemove(player.Index, out _);
|
||||
SkillUtils.SetPlayerInvisibility(player, 0);
|
||||
EntityManager.DestroyPlayerEntities(player.Index);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
private static void CreatePlayerPosProp(CCSPlayerController player)
|
||||
|
|
@ -228,6 +221,26 @@ namespace src.player.skills
|
|||
return bomb.Index;
|
||||
}
|
||||
|
||||
// Wipe spotted state (pawn + carried bomb) so a disguised carrier produces no radar blip.
|
||||
private static void ClearSpottedState(CCSPlayerController player)
|
||||
{
|
||||
var pawn = player.PlayerPawn?.Value;
|
||||
if (pawn != null && pawn.IsValid)
|
||||
{
|
||||
pawn.EntitySpottedState.Spotted = false;
|
||||
pawn.EntitySpottedState.SpottedByMask[0] = 0;
|
||||
pawn.EntitySpottedState.SpottedByMask[1] = 0;
|
||||
}
|
||||
|
||||
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
if (bomb != null && bomb.IsValid && bomb.OwnerEntity?.Index == player.Index)
|
||||
{
|
||||
bomb.EntitySpottedState.Spotted = false;
|
||||
bomb.EntitySpottedState.SpottedByMask[0] = 0;
|
||||
bomb.EntitySpottedState.SpottedByMask[1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#00911f", CsTeam onlyTeam = CsTeam.Terrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", int maxPerServer = -1, Rarity rarity = Rarity.Uncommon) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, maxPerServer, rarity)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace src.player.skills
|
|||
{
|
||||
private const Skills skillName = Skills.Ghost;
|
||||
private static readonly string[] allowedWeapons = [
|
||||
"weapon_molotov", "weapon_incgrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade", "weapon_knife", "weapon_bayonet"
|
||||
"weapon_molotov", "weapon_incgrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade", "weapon_knife", "weapon_bayonet", "weapon_c4"
|
||||
];
|
||||
private static readonly ConcurrentDictionary<uint, byte> invisiblePlayers = [];
|
||||
private const string bloodParticle = "particles/blood_impact/blood_impact_high.vpcf";
|
||||
|
|
@ -61,14 +61,6 @@ namespace src.player.skills
|
|||
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
|
||||
|
||||
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
|
||||
bool isObservingGhost = false;
|
||||
|
||||
if (targetHandle != nint.Zero)
|
||||
{
|
||||
var target = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == targetHandle);
|
||||
var targetInfo = PlayerManager.GetPlayerByIndex(target?.Index);
|
||||
if (targetInfo?.Skill == skillName) isObservingGhost = true;
|
||||
}
|
||||
|
||||
foreach (var playerIndex in invisiblePlayers.Keys)
|
||||
{
|
||||
|
|
@ -79,26 +71,27 @@ namespace src.player.skills
|
|||
if (player.Team == playerController.Team)
|
||||
continue;
|
||||
|
||||
if (!isObservingGhost)
|
||||
{
|
||||
var playerPawn = playerController.PlayerPawn.Value;
|
||||
if (playerPawn == null || !playerPawn.IsValid) continue;
|
||||
var playerPawn = playerController.PlayerPawn.Value;
|
||||
if (playerPawn == null || !playerPawn.IsValid) continue;
|
||||
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
|
||||
if (entity == null || !entity.IsValid) continue;
|
||||
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
|
||||
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
|
||||
continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
|
||||
if (entity == null || !entity.IsValid) continue;
|
||||
|
||||
var bombIndex = GetBombIndex(playerController);
|
||||
if (bombIndex == null) continue;
|
||||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
var bombIndex = GetBombIndex(playerController);
|
||||
if (bombIndex == null) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
}
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,8 +106,6 @@ namespace src.player.skills
|
|||
|
||||
if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0)
|
||||
CreatePlayerPosProp(player);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
private static void CreatePlayerPosProp(CCSPlayerController player)
|
||||
|
|
@ -150,8 +141,6 @@ namespace src.player.skills
|
|||
|
||||
invisiblePlayers.TryRemove(player.Index, out _);
|
||||
EntityManager.DestroyPlayerEntities(player.Index);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void OnTick()
|
||||
|
|
|
|||
|
|
@ -111,8 +111,6 @@ namespace src.player.skills
|
|||
|
||||
SkillUtils.TryGiveWeapon(player, CsItem.SmokeGrenade);
|
||||
SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, grenadeLimit);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
|
|
@ -121,8 +119,6 @@ namespace src.player.skills
|
|||
|
||||
playersWithSkill.TryRemove(player.Index, out _);
|
||||
SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, 1);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5d00ff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", int maxPerServer = -1, Rarity rarity = Rarity.Common, int grenadeLimit = 2) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, maxPerServer, rarity)
|
||||
|
|
|
|||
|
|
@ -44,12 +44,18 @@ namespace src.player.skills
|
|||
|
||||
public static void WeaponPickup(EventItemPickup @event)
|
||||
{
|
||||
var player = PlayerManager.GetPlayerEvent(@event.Userid);
|
||||
if (player == null || !player.IsValid) return;
|
||||
|
||||
var weapon = @event.Item;
|
||||
if (string.IsNullOrEmpty(weapon) || weapon != "c4") return;
|
||||
|
||||
// Gate on the bomb being hot (red), not on `players`: the fresh round's C4 is
|
||||
// handed out before NewRound clears `players`, but a new C4 spawns white, so colour is race-free.
|
||||
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
if (bomb == null || !bomb.IsValid) return;
|
||||
if (bomb.Render.R != 255 || bomb.Render.G != 0 || bomb.Render.B != 0) return;
|
||||
|
||||
var player = PlayerManager.GetPlayerEvent(@event.Userid);
|
||||
if (player == null || !player.IsValid) return;
|
||||
|
||||
player.PrintToCenterAlert(player.GetTranslation("hotbomb_hint"));
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +106,7 @@ namespace src.player.skills
|
|||
{
|
||||
ChangeC4Color(Color.White);
|
||||
foreach (var enemy in Utilities.GetPlayers().Where(p => p.IsValid && p.Team == CsTeam.Terrorist && p.PlayerPawn?.Value?.Health > 0))
|
||||
enemy.PrintToCenterAlert(enemy.GetTranslation("hotbomb_disable"));
|
||||
enemy.PrintToCenterAlert(enemy.GetTranslation("hotbomb_disable_info"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ namespace src.player.skills
|
|||
var relay = EntityManager.CreateTrackedPhysicsProp(player.Index);
|
||||
if (relay == null || !relay.IsValid) return;
|
||||
|
||||
// Register before parenting so the trail is filtered from its first snapshot.
|
||||
activeTrails[player.Index] = relay.Index;
|
||||
|
||||
CBaseEntity target = playerPawn;
|
||||
var entities = EntityManager.GetPlayerEntities(player.Index, "empty_prop");
|
||||
|
||||
|
|
@ -123,8 +126,6 @@ namespace src.player.skills
|
|||
particle.AcceptInput("SetParent", relay, particle, "!activator");
|
||||
particle.AcceptInput("Start");
|
||||
}
|
||||
|
||||
activeTrails[player.Index] = relay.Index;
|
||||
}
|
||||
|
||||
public static void EnableSkill(CCSPlayerController player)
|
||||
|
|
@ -144,8 +145,6 @@ namespace src.player.skills
|
|||
|
||||
mainSkillTimer ??= Instance.AddTimer(2.5f, () => UpdateAllTrails(),
|
||||
CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT | CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
private static void UpdateAllTrails()
|
||||
|
|
@ -168,8 +167,6 @@ namespace src.player.skills
|
|||
mainSkillTimer.Kill();
|
||||
mainSkillTimer = null;
|
||||
}
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#f542ef", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", int maxPerServer = 1, Rarity rarity = Rarity.Common, string particleName = "particles/ui/hud/ui_map_def_utility_trail.vpcf") : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, maxPerServer, rarity)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,13 @@ namespace src.player.skills
|
|||
private static bool hooked = false;
|
||||
private const int actionCode = 503;
|
||||
private static readonly ConcurrentDictionary<uint, byte> playersInAction = [];
|
||||
private static readonly MemoryFunctionVoid<IntPtr, short> Shoot_Secondary = new(GameData.GetSignature("Shoot_Secondary"));
|
||||
private static readonly MemoryFunctionVoid<IntPtr, short>? Shoot_Secondary = ResolveShootSecondary();
|
||||
|
||||
private static MemoryFunctionVoid<IntPtr, short>? ResolveShootSecondary()
|
||||
{
|
||||
try { return new(GameData.GetSignature("Shoot_Secondary")); }
|
||||
catch (Exception ex) { Server.PrintToConsole($"[jRandomSkills] Shoot_Secondary signature unresolved: {ex.Message}"); return null; }
|
||||
}
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
|
|
@ -26,12 +32,12 @@ namespace src.player.skills
|
|||
{
|
||||
playersInAction.Clear();
|
||||
hooked = false;
|
||||
Shoot_Secondary.Unhook(ShootSecondary, HookMode.Pre);
|
||||
Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre);
|
||||
}
|
||||
|
||||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
if (hooked) return;
|
||||
if (hooked || Shoot_Secondary == null) return;
|
||||
hooked = true;
|
||||
Shoot_Secondary.Hook(ShootSecondary, HookMode.Pre);
|
||||
playersInAction.TryAdd(player.Index, 0);
|
||||
|
|
@ -42,7 +48,7 @@ namespace src.player.skills
|
|||
playersInAction.TryRemove(player.Index, out _);
|
||||
if (playersInAction.IsEmpty)
|
||||
{
|
||||
Shoot_Secondary.Unhook(ShootSecondary, HookMode.Pre);
|
||||
Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre);
|
||||
hooked = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,14 +63,6 @@ namespace src.player.skills
|
|||
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
|
||||
|
||||
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
|
||||
bool isObservingNinja = false;
|
||||
|
||||
if (targetHandle != nint.Zero)
|
||||
{
|
||||
var target = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == targetHandle);
|
||||
var targetInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(target)?.Index);
|
||||
if (targetInfo?.Skill == skillName) isObservingNinja = true;
|
||||
}
|
||||
|
||||
foreach (var playerIndex in invisiblePlayers.Keys)
|
||||
{
|
||||
|
|
@ -81,25 +73,26 @@ namespace src.player.skills
|
|||
if (player.Team == playerController.Team)
|
||||
continue;
|
||||
|
||||
if (!isObservingNinja)
|
||||
{
|
||||
var playerPawn = playerController.PlayerPawn.Value;
|
||||
if (playerPawn == null || !playerPawn.IsValid) continue;
|
||||
var playerPawn = playerController.PlayerPawn.Value;
|
||||
if (playerPawn == null || !playerPawn.IsValid) continue;
|
||||
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
|
||||
if (entity == null || !entity.IsValid) continue;
|
||||
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
|
||||
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
|
||||
continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
|
||||
if (entity == null || !entity.IsValid) continue;
|
||||
|
||||
var bombIndex = GetBombIndex(playerController);
|
||||
if (bombIndex == null) continue;
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
}
|
||||
var bombIndex = GetBombIndex(playerController);
|
||||
if (bombIndex == null) continue;
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,8 +132,6 @@ namespace src.player.skills
|
|||
|
||||
if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0)
|
||||
CreatePlayerPosProp(player);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
|
|
@ -148,8 +139,6 @@ namespace src.player.skills
|
|||
SkillUtils.SetPlayerInvisibility(player, 0);
|
||||
invisiblePlayers.TryRemove(player.Index, out _);
|
||||
EntityManager.DestroyPlayerEntities(player.Index);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
private static void CreatePlayerPosProp(CCSPlayerController player)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using src.utils;
|
||||
|
|
@ -12,10 +13,19 @@ namespace src.player.skills
|
|||
{
|
||||
private const Skills skillName = Skills.Planter;
|
||||
private static readonly ConcurrentDictionary<uint, float> plantingPlayers = [];
|
||||
// mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override.
|
||||
private static int defaultC4Timer = 40;
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue<int>() ?? 40;
|
||||
}
|
||||
|
||||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
// At round start (not at plant) so the client HUD/alert countdown is right before the plant completes.
|
||||
Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "extraC4BlowTime")}");
|
||||
}
|
||||
|
||||
public static void BombBeginplant(EventBombBeginplant @event)
|
||||
|
|
@ -59,6 +69,8 @@ namespace src.player.skills
|
|||
foreach (var player in Utilities.GetPlayers())
|
||||
DisableSkill(player);
|
||||
plantingPlayers.Clear();
|
||||
|
||||
Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}");
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ namespace src.player.skills
|
|||
private static void SetEnemiesVisibleOnRadar(CCSPlayerController player)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.PlayerPawn?.Value == null) return;
|
||||
int playerIndex = (int)player.Index - 1;
|
||||
|
||||
// SpottedByMask is indexed by player slot (0-63), not entity index.
|
||||
int slot = player.Slot;
|
||||
|
||||
foreach (var enemy in Utilities.GetPlayers().FindAll(p => p.Team != player.Team))
|
||||
{
|
||||
|
|
@ -39,9 +41,13 @@ namespace src.player.skills
|
|||
if (enemyEvent == null || !enemyEvent.IsValid) continue;
|
||||
|
||||
var enemyPawn = enemyEvent.PlayerPawn.Value;
|
||||
if (enemyPawn == null) continue;
|
||||
if (enemyPawn == null || !enemyPawn.IsValid) continue;
|
||||
|
||||
enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (int)(playerIndex % 32));
|
||||
// Invisibility (low render alpha) beats the radar hack.
|
||||
if (enemyPawn.Render.A < 200) continue;
|
||||
|
||||
// Only the observer's slot bit — the Spotted bool would reveal to the whole team.
|
||||
enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32));
|
||||
}
|
||||
|
||||
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
|
||||
|
|
@ -49,7 +55,7 @@ namespace src.player.skills
|
|||
{
|
||||
var bomb = bombEntities.FirstOrDefault();
|
||||
if (bomb != null && bomb.IsValid)
|
||||
bomb.EntitySpottedState.SpottedByMask[0] |= (1u << (int)(playerIndex % 32));
|
||||
bomb.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using src.utils;
|
||||
using static src.jRandomSkills;
|
||||
|
|
@ -15,6 +16,8 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue<int>() ?? 16000;
|
||||
|
||||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
|
||||
|
|
@ -24,7 +27,7 @@ namespace src.player.skills
|
|||
var moneyServices = player.InGameMoneyServices;
|
||||
if (moneyServices == null) return;
|
||||
|
||||
moneyBonus = Math.Min(moneyBonus, 16000 - moneyServices.Account);
|
||||
moneyBonus = Math.Min(moneyBonus, GetMaxMoney() - moneyServices.Account);
|
||||
|
||||
playerInfo.SkillChance = moneyBonus;
|
||||
AddMoney(player, moneyBonus);
|
||||
|
|
@ -48,7 +51,7 @@ namespace src.player.skills
|
|||
var moneyServices = player.InGameMoneyServices;
|
||||
if (moneyServices == null) return;
|
||||
|
||||
moneyServices.Account = Math.Clamp(moneyServices.Account + money, minimum, 16000);
|
||||
moneyServices.Account = Math.Clamp(moneyServices.Account + money, minimum, GetMaxMoney());
|
||||
Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using static src.jRandomSkills;
|
||||
using System.Collections.Concurrent;
|
||||
|
|
@ -10,7 +11,7 @@ namespace src.player.skills
|
|||
public class SecondLife : ISkill
|
||||
{
|
||||
private const Skills skillName = Skills.SecondLife;
|
||||
private static readonly ConcurrentDictionary<nint, int> secondLifePlayers = [];
|
||||
private static readonly ConcurrentDictionary<nint, byte> usedThisRound = [];
|
||||
private static readonly object setLock = new();
|
||||
|
||||
public static void LoadSkill()
|
||||
|
|
@ -20,30 +21,53 @@ namespace src.player.skills
|
|||
|
||||
public static void NewRound()
|
||||
{
|
||||
secondLifePlayers.Clear();
|
||||
usedThisRound.Clear();
|
||||
}
|
||||
|
||||
public static void PlayerHurt(EventPlayerHurt @event)
|
||||
// Must intercept pre-damage: player_hurt fires after the death is committed, so a
|
||||
// lethal hit (esp. fall damage) can't be undone there. Cancelling the blow here keeps the respawn clean.
|
||||
public static void OnTakeDamage(DynamicHook h)
|
||||
{
|
||||
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
|
||||
var victimEntity = h.GetParam<CEntityInstance>(0);
|
||||
var info = h.GetParam<CTakeDamageInfo>(1);
|
||||
if (victimEntity == null || !victimEntity.IsValid || info == null) return;
|
||||
|
||||
if (!Instance.IsPlayerValid(victim)) return;
|
||||
var victimInfo = PlayerManager.GetPlayerByIndex(victim!.Index);
|
||||
var victimPawn = victimEntity.As<CCSPlayerPawn>();
|
||||
if (victimPawn == null || !victimPawn.IsValid || victimPawn.DesignerName != "player") return;
|
||||
|
||||
var victimController = victimPawn.Controller.Value;
|
||||
if (victimController == null || !victimController.IsValid) return;
|
||||
|
||||
var victim = victimController.As<CCSPlayerController>();
|
||||
if (victim == null || !victim.IsValid || !victim.PawnIsAlive) return;
|
||||
|
||||
var victimInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(victim)?.Index ?? victim.Index));
|
||||
if (victimInfo == null || victimInfo.Skill != skillName) return;
|
||||
|
||||
var victimPawn = victim!.PlayerPawn.Value;
|
||||
if (victimPawn!.Health > 0 || (secondLifePlayers.TryGetValue(victim.Handle, out int tick) && tick + 4 < Server.TickCount))
|
||||
return;
|
||||
if (info.Damage < victimPawn.Health) return;
|
||||
if (usedThisRound.ContainsKey(victim.Handle)) return;
|
||||
|
||||
lock (setLock)
|
||||
{
|
||||
SetHealth(victim, SkillsInfo.GetValue<int>(skillName, "startHealth"));
|
||||
if (usedThisRound.ContainsKey(victim.Handle)) return;
|
||||
|
||||
var spawnpoint = SkillUtils.GetSpawnPointVector(victim);
|
||||
if (spawnpoint == null) return;
|
||||
if (spawnpoint == null) return; // no clean respawn point -> let the normal death happen
|
||||
|
||||
victimPawn.Teleport(spawnpoint, null, null);
|
||||
secondLifePlayers.TryAdd(victim.Handle, Server.TickCount);
|
||||
usedThisRound.TryAdd(victim.Handle, 0);
|
||||
info.Damage = 0;
|
||||
|
||||
int startHealth = SkillsInfo.GetValue<int>(skillName, "startHealth");
|
||||
Server.NextFrame(() =>
|
||||
{
|
||||
if (victim == null || !victim.IsValid) return;
|
||||
var pawn = victim.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) return;
|
||||
|
||||
pawn.Health = startHealth;
|
||||
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
|
||||
pawn.Teleport(spawnpoint, null, new Vector(0, 0, 0));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +78,7 @@ namespace src.player.skills
|
|||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
{
|
||||
secondLifePlayers.TryRemove(player.Handle, out _);
|
||||
usedThisRound.TryRemove(player.Handle, out _);
|
||||
if (player.PlayerPawn.Value == null) return;
|
||||
SetHealth(player, Math.Min(player.PlayerPawn.Value.Health + SkillsInfo.GetValue<int>(skillName, "startHealth"), 100));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ namespace src.player.skills
|
|||
var attackerPawn = attacker.PlayerPawn.Value;
|
||||
if (attackerPawn == null || !attackerPawn.IsValid) return false;
|
||||
|
||||
var victimPawn = attacker.PlayerPawn.Value;
|
||||
var victimPawn = victim.PlayerPawn.Value;
|
||||
if (victimPawn == null || !victimPawn.IsValid) return false;
|
||||
|
||||
var result = RayTrace.TraceHullShape(
|
||||
|
|
@ -123,7 +123,11 @@ namespace src.player.skills
|
|||
Vector direction = SkillUtils.GetForwardVector(targetAngle);
|
||||
Vector targetPos = victimPos - (direction * distance);
|
||||
|
||||
if (CheckTeleport(attacker, victim, victimPos, targetPos, targetAngle))
|
||||
// Trace can only ignore one entity (victim); start a step out so the attacker's
|
||||
// own body in melee range doesn't clip the hull and report a false "no space".
|
||||
Vector traceStart = victimPos - (direction * 20f);
|
||||
|
||||
if (CheckTeleport(attacker, victim, traceStart, targetPos, targetAngle))
|
||||
{
|
||||
attackerPawn.Teleport(targetPos, targetAngle, Vector.Zero);
|
||||
teleported = true;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using src.utils;
|
||||
using static src.jRandomSkills;
|
||||
|
|
@ -9,10 +10,24 @@ namespace src.player.skills
|
|||
public class ShortBomb : ISkill
|
||||
{
|
||||
private const Skills skillName = Skills.ShortBomb;
|
||||
// mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override.
|
||||
private static int defaultC4Timer = 40;
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue<int>() ?? 40;
|
||||
}
|
||||
|
||||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
// At round start (not at plant) so the client HUD/alert countdown is right before the plant completes.
|
||||
Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "detonationTime")}");
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}");
|
||||
}
|
||||
|
||||
public static void BombPlanted(EventBombPlanted @event)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@ namespace src.player.skills
|
|||
|
||||
public static bool OnWeaponCanAcquire(DynamicHook hook, CCSPlayerController player, CEconItemView econItem, CCSWeaponBaseVData vdata)
|
||||
{
|
||||
// vdata can be non-null yet point at freed/invalid schema memory; reading
|
||||
// .Name then throws NullReferenceException inside get_Name(). Guard the handle.
|
||||
if (vdata == null || vdata.Handle == IntPtr.Zero) return false;
|
||||
|
||||
string weaponName = vdata.Name;
|
||||
if (string.IsNullOrEmpty(weaponName)) return false;
|
||||
|
||||
|
|
@ -123,15 +127,12 @@ namespace src.player.skills
|
|||
public static void EnableSkill(CCSPlayerController _)
|
||||
{
|
||||
Event.EnableTransmit();
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
{
|
||||
if (knivesInfo.TryRemove(player.Index, out KnifeInfo? knifeInfo) && knifeInfo != null)
|
||||
DisableKnifeSkill(knifeInfo);
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void DisableKnifeSkill(KnifeInfo knifeInfo)
|
||||
|
|
|
|||
|
|
@ -28,19 +28,30 @@ namespace src.player.skills
|
|||
temporaryBlockList.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
|
||||
// One pawn->info map per frame instead of a GetPlayers scan per client.
|
||||
var infoByPawnHandle = new Dictionary<nint, jSkill_PlayerInfo?>();
|
||||
foreach (var p in Utilities.GetPlayers())
|
||||
{
|
||||
var h = p?.Pawn?.Value?.Handle;
|
||||
if (p != null && p.IsValid && h != null)
|
||||
infoByPawnHandle[h.Value] = PlayerManager.GetPlayerByIndex(p.Index);
|
||||
}
|
||||
|
||||
foreach (var (info, player) in infoList)
|
||||
{
|
||||
if (player == null || !player.IsValid) continue;
|
||||
|
||||
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;
|
||||
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget?.Value?.Handle ?? nint.Zero;
|
||||
jSkill_PlayerInfo? observerInfo = null;
|
||||
if (targetHandle != nint.Zero)
|
||||
infoByPawnHandle.TryGetValue(targetHandle, out observerInfo);
|
||||
|
||||
foreach (var kvp in glows)
|
||||
{
|
||||
var enemyIndex = kvp.Key;
|
||||
var glowInfo = kvp.Value;
|
||||
var enemy = Utilities.GetPlayers().FirstOrDefault(e => e != null && e.IsValid && e.Index == enemyIndex);
|
||||
var enemy = Utilities.GetPlayerFromIndex((int)enemyIndex);
|
||||
|
||||
bool hasSkill = playerInfo?.Skill == skillName;
|
||||
bool observerHasSkill = observerInfo != null && observerInfo?.Skill == skillName;
|
||||
|
|
@ -85,8 +96,9 @@ namespace src.player.skills
|
|||
var relayIndex = kvp.Value.RelayIndex;
|
||||
var glowIndex = kvp.Value.GlowIndex;
|
||||
|
||||
SkillUtils.SafeKillEntity<CDynamicProp>(relayIndex);
|
||||
// The glow follows the relay, so it dies first.
|
||||
SkillUtils.SafeKillEntity<CDynamicProp>(glowIndex);
|
||||
SkillUtils.SafeKillEntity<CDynamicProp>(relayIndex);
|
||||
|
||||
temporaryBlockList.TryAdd(relayIndex, DateTime.Now.AddSeconds(2));
|
||||
temporaryBlockList.TryAdd(glowIndex, DateTime.Now.AddSeconds(2));
|
||||
|
|
@ -103,8 +115,6 @@ namespace src.player.skills
|
|||
|
||||
if (glows.IsEmpty)
|
||||
SetGlowEffectForAll();
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
|
|
@ -116,8 +126,6 @@ namespace src.player.skills
|
|||
NewRound();
|
||||
return;
|
||||
}
|
||||
|
||||
SkillUtils.ForceFullUpdateToAll();
|
||||
}
|
||||
|
||||
private static void SetGlowEffectForAll()
|
||||
|
|
@ -149,6 +157,9 @@ namespace src.player.skills
|
|||
if (modelGlow == null || !modelGlow.IsValid || modelRelay == null || !modelRelay.IsValid)
|
||||
continue;
|
||||
|
||||
// Register before spawn so the glow is filtered from its first snapshot.
|
||||
glows.TryAdd(enemy.Index, (modelRelay.Index, modelGlow.Index, enemy.Team));
|
||||
|
||||
var relayEntity = modelRelay.CBodyComponent?.SceneNode?.Owner?.Entity;
|
||||
if (relayEntity != null)
|
||||
relayEntity.Flags = (uint)(relayEntity.Flags & ~(1 << 2));
|
||||
|
|
@ -180,8 +191,6 @@ namespace src.player.skills
|
|||
|
||||
modelRelay.AcceptInput("FollowEntity", enemyPawn, modelRelay, "!activator");
|
||||
modelGlow.AcceptInput("FollowEntity", modelRelay, modelGlow, "!activator");
|
||||
|
||||
glows.TryAdd(enemy.Index, (modelRelay.Index, modelGlow.Index, enemy.Team));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
// https://discord.com/channels/1160907911501991946/1508172390863994910/1508180670659166348
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
|
||||
namespace jRandomSkills.src.utils
|
||||
{
|
||||
|
||||
#region Native Structs
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct CUtlMemory<T> where T : unmanaged
|
||||
{
|
||||
public T* m_pMemory;
|
||||
public int m_nAllocationCount;
|
||||
public int m_nGrowSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct CUtlVector<T> where T : unmanaged
|
||||
{
|
||||
public int m_Size;
|
||||
public CUtlMemory<T> m_Memory;
|
||||
|
||||
public int Count => m_Size;
|
||||
|
||||
public ref T Element(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Size)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return ref m_Memory.m_pMemory[index];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Services
|
||||
|
||||
public class INetworkServerService : NativeObject
|
||||
{
|
||||
private readonly VirtualFunctionWithReturn<nint, nint> GetIGameServerFunc;
|
||||
|
||||
public INetworkServerService() : base(NativeAPI.GetValveInterface(0, "NetworkServerService_001"))
|
||||
{
|
||||
GetIGameServerFunc = new VirtualFunctionWithReturn<nint, nint>(Handle, GameData.GetOffset("INetworkServerService_GetIGameServer"));
|
||||
}
|
||||
|
||||
public INetworkGameServer GetIGameServer()
|
||||
{
|
||||
return new INetworkGameServer(GetIGameServerFunc.Invoke(Handle));
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe class INetworkGameServer(nint ptr) : NativeObject(ptr)
|
||||
{
|
||||
private static readonly int SlotsOffset = GameData.GetOffset("INetworkGameServer_Slots");
|
||||
|
||||
private ref CUtlVector<nint> Slots => ref Unsafe.AsRef<CUtlVector<nint>>((void*)(Handle + SlotsOffset));
|
||||
|
||||
public CServerSideClient? GetClientBySlot(int slot)
|
||||
{
|
||||
if (slot < 0 || slot >= Slots.Count)
|
||||
return null;
|
||||
|
||||
var ptr = Slots.Element(slot);
|
||||
|
||||
if (ptr == nint.Zero)
|
||||
return null;
|
||||
|
||||
return new CServerSideClient(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CServerSideClient
|
||||
|
||||
public unsafe class CServerSideClient(nint ptr) : NativeObject(ptr)
|
||||
{
|
||||
private static readonly int m_nDeltaTick = GameData.GetOffset("CServerSideClient_m_nDeltaTick");
|
||||
|
||||
private ref T Field<T>(int offset) where T : unmanaged
|
||||
{
|
||||
return ref Unsafe.AsRef<T>((void*)(Handle + offset));
|
||||
}
|
||||
|
||||
public int DeltaTick
|
||||
{
|
||||
get => Field<int>(m_nDeltaTick);
|
||||
set => Field<int>(m_nDeltaTick) = value;
|
||||
}
|
||||
|
||||
public void ForceFullUpdate() => DeltaTick = -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -21,6 +21,22 @@ namespace src.utils
|
|||
public DateTime CreatedAt;
|
||||
}
|
||||
|
||||
private const int EntityBudget = 3500;
|
||||
private static int _cachedCount;
|
||||
private static int _cachedCountTick = -1000000;
|
||||
|
||||
public static bool OverBudget()
|
||||
{
|
||||
int tick = Server.TickCount;
|
||||
if (tick - _cachedCountTick > 64 || tick < _cachedCountTick)
|
||||
{
|
||||
_cachedCountTick = tick;
|
||||
try { _cachedCount = Utilities.GetAllEntities().Count(); }
|
||||
catch { _cachedCount = 0; }
|
||||
}
|
||||
return _cachedCount >= EntityBudget;
|
||||
}
|
||||
|
||||
public static void RegisterEntity(uint entityIndex, uint playerIndex, string entityType)
|
||||
{
|
||||
if (entityIndex == 0) return;
|
||||
|
|
@ -59,6 +75,7 @@ namespace src.utils
|
|||
{
|
||||
try
|
||||
{
|
||||
if (OverBudget()) return null;
|
||||
var particle = Utilities.CreateEntityByName<CParticleSystem>("info_particle_system");
|
||||
if (particle == null || !particle.IsValid) return null;
|
||||
|
||||
|
|
@ -81,6 +98,7 @@ namespace src.utils
|
|||
{
|
||||
try
|
||||
{
|
||||
if (OverBudget()) return null;
|
||||
var prop = Utilities.CreateEntityByName<CDynamicProp>(designerName);
|
||||
if (prop == null || !prop.IsValid) return null;
|
||||
|
||||
|
|
@ -103,6 +121,7 @@ namespace src.utils
|
|||
{
|
||||
try
|
||||
{
|
||||
if (OverBudget()) return null;
|
||||
var shake = Utilities.CreateEntityByName<CEnvShake>("env_shake");
|
||||
if (shake == null || !shake.IsValid) return null;
|
||||
|
||||
|
|
@ -121,6 +140,7 @@ namespace src.utils
|
|||
{
|
||||
try
|
||||
{
|
||||
if (OverBudget()) return null;
|
||||
var chicken = Utilities.CreateEntityByName<CChicken>("chicken");
|
||||
if (chicken == null || !chicken.IsValid) return null;
|
||||
|
||||
|
|
@ -139,6 +159,7 @@ namespace src.utils
|
|||
{
|
||||
try
|
||||
{
|
||||
if (OverBudget()) return null;
|
||||
var prop = Utilities.CreateEntityByName<CPhysicsPropMultiplayer>("prop_physics_multiplayer");
|
||||
if (prop == null || !prop.IsValid) return null;
|
||||
|
||||
|
|
@ -158,6 +179,7 @@ namespace src.utils
|
|||
|
||||
try
|
||||
{
|
||||
if (OverBudget()) return null;
|
||||
var trigger = Utilities.CreateEntityByName<CTriggerMultiple>("trigger_multiple");
|
||||
if (trigger == null || trigger.AbsOrigin == null) return null;
|
||||
|
||||
|
|
@ -218,16 +240,41 @@ namespace src.utils
|
|||
}
|
||||
}
|
||||
|
||||
public static bool DestroyEntity(uint entityIndex)
|
||||
// Dying entities stay out of transmit until the engine processes the kill (Event.CheckTransmit).
|
||||
private static readonly ConcurrentDictionary<uint, DateTime> recentlyDestroyed = new();
|
||||
|
||||
public static List<uint> GetRecentlyDestroyedSnapshot()
|
||||
{
|
||||
if (recentlyDestroyed.IsEmpty) return [];
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var result = new List<uint>();
|
||||
foreach (var kvp in recentlyDestroyed)
|
||||
{
|
||||
if (now > kvp.Value) recentlyDestroyed.TryRemove(kvp.Key, out _);
|
||||
else result.Add(kvp.Key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool SuppressKills = false;
|
||||
|
||||
public static bool DestroyEntity(uint entityIndex, float delay = 0.1f)
|
||||
{
|
||||
trackedEntities.TryRemove(entityIndex, out _);
|
||||
|
||||
if (SuppressKills)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)entityIndex);
|
||||
if (entity != null && entity.IsValid)
|
||||
{
|
||||
entity.AddEntityIOEvent("Kill", entity, delay: 0.1f);
|
||||
recentlyDestroyed[entityIndex] = DateTime.UtcNow.AddSeconds(delay + 2.0);
|
||||
// Detach first so no follower is left on a freed parent.
|
||||
entity.AcceptInput("ClearParent");
|
||||
entity.AddEntityIOEvent("Kill", entity, delay: delay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -247,14 +294,33 @@ namespace src.utils
|
|||
|
||||
public static void DestroyPlayerEntities(uint playerIndex)
|
||||
{
|
||||
foreach (var entityIndex in GetPlayerEntities(playerIndex).ToList())
|
||||
// Children die first (reverse creation order), same frame.
|
||||
var ordered = trackedEntities
|
||||
.Where(kvp => kvp.Value.PlayerIndex == playerIndex)
|
||||
.OrderByDescending(kvp => kvp.Value.CreatedAt)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var entityIndex in ordered)
|
||||
DestroyEntity(entityIndex);
|
||||
}
|
||||
|
||||
public static void DestroyAllTracked()
|
||||
{
|
||||
foreach (var entityIndex in trackedEntities.Keys.ToList())
|
||||
DestroyEntity(entityIndex);
|
||||
// Stagger between owners; each owner's chain dies child-first in one frame.
|
||||
int group = 0;
|
||||
foreach (var owner in trackedEntities.Values.Select(e => e.PlayerIndex).Distinct().ToList())
|
||||
{
|
||||
float delay = 0.1f + (group++ % 16) * 0.03f;
|
||||
var ordered = trackedEntities
|
||||
.Where(kvp => kvp.Value.PlayerIndex == owner)
|
||||
.OrderByDescending(kvp => kvp.Value.CreatedAt)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var entityIndex in ordered)
|
||||
DestroyEntity(entityIndex, delay);
|
||||
}
|
||||
|
||||
trackedEntities.Clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,6 +172,14 @@ namespace src.utils
|
|||
return key;
|
||||
}
|
||||
|
||||
// Resolves and caches the player's language at connect time, so the GeoLite
|
||||
// database lookup never runs on the tick path.
|
||||
public static void PreResolveLanguage(CCSPlayerController? player)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.IsBot) return;
|
||||
GetLangCode(player);
|
||||
}
|
||||
|
||||
private static bool _geoLiteBroken = false;
|
||||
|
||||
private static string GetLangCode(CCSPlayerController? player)
|
||||
|
|
|
|||
|
|
@ -333,76 +333,6 @@ namespace src.utils
|
|||
return EntityManager.CreateTrackedTrigger(ownerPlayerIndex, name, radius, pos);
|
||||
}
|
||||
|
||||
public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null, INetworkGameServer? networkGameServer = null)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
"signatures": {
|
||||
"library": "server",
|
||||
"windows": "48 8B C4 48 89 58 ? 48 89 68 ? 48 89 70 ? 57 41 56 41 57 48 81 EC ? ? ? ? 48 8B B4 24 ? ? ? ? 4D 8B F8",
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 45 89 CF 41 56 49 89 FE"
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 45 89 CE 41 55 4D 89 C5 41 54 53 48 83 EC ? 48 89 55 ? 48 89 F2 48 89 FE"
|
||||
}
|
||||
},
|
||||
"HEGrenadeProjectile_CreateFunc": {
|
||||
"signatures": {
|
||||
"library": "server",
|
||||
"windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B AC 24 ? ? ? ? 49 8B F8",
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 D7"
|
||||
"windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B 6C 24 ? 49 8B F8 4C 8B C2 0F 29 74 24 ? 48 8B D1 48 8B D9 48 8D 0D ? ? ? ? 4C 8B CD E8 ? ? ? ? F3 0F 10 0D ? ? ? ? 48 8B C8 48 8B F0 E8 ? ? ? ? 48 8B D7 48 8B CE",
|
||||
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 49 89 D6 48 89 F2 48 89 FE 41 55 48 8D 3D ? ? ? ? 4D 89 C5 41 54 45 89 CC 53"
|
||||
}
|
||||
},
|
||||
"Shoot_Secondary": {
|
||||
|
|
@ -24,25 +24,7 @@
|
|||
"signatures": {
|
||||
"library": "server",
|
||||
"windows": "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 48 8D 6C 24 ? 48 81 EC ? ? ? ? 48 8B DA 48 8B F1 48 8D 55",
|
||||
"linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC"
|
||||
}
|
||||
},
|
||||
"CServerSideClient_m_nDeltaTick": {
|
||||
"offsets": {
|
||||
"windows": 348,
|
||||
"linux": 348
|
||||
}
|
||||
},
|
||||
"INetworkGameServer_Slots": {
|
||||
"offsets": {
|
||||
"windows": 592,
|
||||
"linux": 592
|
||||
}
|
||||
},
|
||||
"INetworkServerService_GetIGameServer": {
|
||||
"offsets": {
|
||||
"windows": 23,
|
||||
"linux": 24
|
||||
"linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC ? ? ? ? E8"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue