v1.2.3.b6
New skills (9) - Bounty: put a cash price on an enemy's head, the killer collects it. - EmpGrenade: grenade victims lose radar and crosshair for a while. - GravityDecoy: the decoy alters gravity for everyone in its radius. - Heavyweight: immune to push and slow effects from other skills. - Nemesis: the marked player takes extra damage from everyone. - Punisher: banks part of the damage you take and releases it on your next hit. - Rewind: drop a marker and teleport back to it shortly after. - TrueArmor: armor absorbs part of incoming damage instead of health. - Voodoo: the marked enemy shares the damage you take. Shared systems - DecoyRing: reusable ground ring so decoy skills show their real area of effect. Adopted by FireRain, FrozenDecoy, GravityDecoy and MagneticDecoy. - WeaponPool: single cached weapon classification (rifle/pistol/grenade) with a normalizing lookup, backed by the new config Weapons section. Replaces the duplicated hardcoded arrays in Assassin, Chicken, Ghost, NoNades, PrimaryBan, RandomWeapon, SniperElite and WeaponsSwap. [because of compatibility with WeaponRestrict] - HUD suppression registry in SkillUtils: crosshair and radar hiding is now refcounted per owner, so overlapping skills (Glitch, Jammer, EmpGrenade) cannot restore each other's state early. - SkillUtils.ResolveHiddenPawns: shared invisible-pawn resolution for C4Camouflage, Ghost and Ninja. - Config gained a Weapons section with automatic migration for existing installs, plus a per-skill DisableOnPistolRound switch honoured by both the round draw and css_setskill. Sound - Volume rebalance - Add SkillUtils.EmitSoundToPlayer with a private RecipientFilter. A null filter had been broadcasting every skill sound to all clients. Migrated 16 call sites: Poison, DemonEye, ToxicSmoke, CarefulBullets, Thorns, HotBomb, LastGasp, LongKnife, Nightmare, Jester, Tripwire, Medic, HealingSmoke, Rewind, Watchmaker. - New effects: LongKnife knife-hit feedback (Weapon_Knife.Hit.Heavy/Light), Grapple hook impact, Fortnite barricade placement, Ricochet bounce, Nightmare cursed tone, Replicator/Illusionist spawn, PsychicDefusing defuse cue, TakeAmmo pickup, Tripwire and Jammer trigger tones. - 29 skills gained a configurable SoundVolume, LongKnife also HitSoundVolume. - Grapple: emit the hook impact at the hook entity instead of at the owner so the enemy team can locate it. - PawelJumper: the engine already plays the first jump, only extra jumps emit. Models and visuals - Grapple: real hook model with configurable hookScale and hookEmbed so the hook sits in the surface it hit. - Pilot: glowing exhaust trail parented to the pawn, active only while airborne. Performance - SkillsInfo.GetValue now resolves through a ConcurrentDictionary cache instead of reflecting on every call; invalidated on config reload. - HUD render cache keyed on player, config, description flag and the three source lines; identical frames skip the rebuild. - Glaz.CheckTransmit: hoist smoke index resolution out of the per-viewer loop and build the pawn-owner map lazily. - playersSkills switched from ConcurrentBag to HashSet<Skills>, turning the NoRepeat filter from an O(n) scan into a set lookup. Fixes - NoRepeat could hand a player two or three skills in one round because the history bag was never deduplicated; the HashSet rewrite closes it. - SniperElite: restore the saved weapon after the halftime team swap instead of dropping it on the pistol round. - Illiterate: the holder alert now reaches chat and is exempt from its own scrambler via PrintToChat(ignoreIlliterate). - EntityManager.DestroyBeam: zero out width and render colour before the kill so beams cannot linger visibly, with a next-frame Remove fallback. - Ricochet: drop all 18 diagnostic Trace calls and the Trace helper; the skill measured stable over 48h and the tracing dominated the debug log. Debug - Tag round start, round end and freeze-time-end lines with [WARMUP] so warmup load is separable from live rounds.
This commit is contained in:
parent
615c55b2da
commit
b2d2c5bf9a
93 changed files with 3414 additions and 329 deletions
|
|
@ -33,6 +33,9 @@ namespace src.utils
|
|||
using (var sr = new StreamReader(fs))
|
||||
json = sr.ReadToEnd();
|
||||
newConfig = JsonConvert.DeserializeObject<SettingsModel>(json) ?? new SettingsModel();
|
||||
|
||||
if (IsSectionMissing(json, nameof(SettingsModel.Weapons)))
|
||||
SaveConfig(newConfig);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -45,6 +48,18 @@ namespace src.utils
|
|||
}
|
||||
}
|
||||
|
||||
private static bool IsSectionMissing(string json, string section)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Newtonsoft.Json.Linq.JObject.Parse(json)[section] == null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveConfig(SettingsModel config)
|
||||
{
|
||||
lock (fileLock)
|
||||
|
|
@ -92,6 +107,7 @@ namespace src.utils
|
|||
public string DisableHUDOnDeathPermission { get; set; }
|
||||
public bool DisableSkillsOnRoundEnd { get; set; }
|
||||
public int? CurseSkillPerPlayer { get; set; }
|
||||
public WeaponPools Weapons { get; set; }
|
||||
public LanguageSystem LanguageSystem { get; set; }
|
||||
public HtmlHudCustomisation HtmlHudCustomisation { get; set; }
|
||||
public ChatMessage ChatMessage { get; set; }
|
||||
|
|
@ -124,6 +140,31 @@ namespace src.utils
|
|||
DisableSkillsOnRoundEnd = false;
|
||||
CurseSkillPerPlayer = null;
|
||||
|
||||
Weapons = new WeaponPools
|
||||
{
|
||||
Rifle =
|
||||
[
|
||||
"weapon_ak47", "weapon_m4a1", "weapon_m4a1_silencer",
|
||||
"weapon_famas", "weapon_galilar", "weapon_aug", "weapon_sg556",
|
||||
"weapon_mp9", "weapon_mac10", "weapon_bizon", "weapon_mp7",
|
||||
"weapon_ump45", "weapon_p90", "weapon_mp5sd", "weapon_ssg08",
|
||||
"weapon_awp", "weapon_scar20", "weapon_g3sg1", "weapon_nova",
|
||||
"weapon_xm1014", "weapon_mag7", "weapon_sawedoff", "weapon_m249",
|
||||
"weapon_negev"
|
||||
],
|
||||
Pistol =
|
||||
[
|
||||
"weapon_deagle", "weapon_revolver", "weapon_glock", "weapon_usp_silencer",
|
||||
"weapon_cz75a", "weapon_fiveseven", "weapon_p250", "weapon_tec9",
|
||||
"weapon_elite", "weapon_hkp2000"
|
||||
],
|
||||
Grenade =
|
||||
[
|
||||
"weapon_hegrenade", "weapon_flashbang", "weapon_smokegrenade",
|
||||
"weapon_molotov", "weapon_incgrenade", "weapon_decoy"
|
||||
],
|
||||
};
|
||||
|
||||
LanguageSystem = new LanguageSystem
|
||||
{
|
||||
DefaultLangCode = "en",
|
||||
|
|
@ -204,6 +245,13 @@ namespace src.utils
|
|||
}
|
||||
}
|
||||
|
||||
public class WeaponPools
|
||||
{
|
||||
public required List<string> Rifle { get; set; }
|
||||
public required List<string> Pistol { get; set; }
|
||||
public required List<string> Grenade { get; set; }
|
||||
}
|
||||
|
||||
public class ChatMessage
|
||||
{
|
||||
public required float MaxWidth { get; set; }
|
||||
|
|
|
|||
204
jRandomSkills - SRC Files/src/utils/DecoyRing.cs
Normal file
204
jRandomSkills - SRC Files/src/utils/DecoyRing.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Entities.Constants;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using src.player;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
|
||||
namespace src.utils
|
||||
{
|
||||
public static class DecoyRing
|
||||
{
|
||||
private const string discModel = "models/dev/grenade_trajectory/grenade_target.vmdl";
|
||||
|
||||
private const float discBaseRadius = 13.462f;
|
||||
private const int discAlpha = 178;
|
||||
|
||||
private const int outerSegments = 24;
|
||||
private const int innerSegments = 18;
|
||||
private const float ringHeight = 3f;
|
||||
|
||||
private const float maxLifetime = 25f;
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Ring> rings = [];
|
||||
|
||||
private sealed class Ring
|
||||
{
|
||||
public uint? Disc { get; set; }
|
||||
public List<uint> Beams { get; } = [];
|
||||
}
|
||||
|
||||
public static void PreloadAssets()
|
||||
{
|
||||
jRandomSkills.Instance.AddToManifest(discModel);
|
||||
}
|
||||
|
||||
public static void Show(Skills skill, uint id, Vector center, float radius)
|
||||
{
|
||||
if (radius <= 0) return;
|
||||
|
||||
string key = MakeKey(skill, id);
|
||||
var ring = new Ring();
|
||||
if (!rings.TryAdd(key, ring)) return;
|
||||
|
||||
SpawnDisc(center, radius, ring);
|
||||
|
||||
if (!EntityManager.OverBudget())
|
||||
{
|
||||
Color color = GetSkillColor(skill);
|
||||
SpawnRing(center, radius, outerSegments, color, ring);
|
||||
SpawnRing(center, radius * .5f, innerSegments, color, ring);
|
||||
}
|
||||
|
||||
jRandomSkills.Instance.AddTimer(maxLifetime, () => Hide(skill, id), TimerFlags.STOP_ON_MAPCHANGE);
|
||||
}
|
||||
|
||||
public static void Hide(Skills skill, uint id)
|
||||
{
|
||||
if (!rings.TryRemove(MakeKey(skill, id), out var ring)) return;
|
||||
Destroy(ring);
|
||||
}
|
||||
|
||||
public static void ClearAll(Skills skill)
|
||||
{
|
||||
string prefix = skill + "|";
|
||||
|
||||
foreach (var key in rings.Keys)
|
||||
{
|
||||
if (!key.StartsWith(prefix, StringComparison.Ordinal)) continue;
|
||||
if (!rings.TryRemove(key, out var ring)) continue;
|
||||
|
||||
Destroy(ring);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Destroy(Ring ring)
|
||||
{
|
||||
foreach (uint beamIndex in ring.Beams)
|
||||
EntityManager.DestroyBeam(beamIndex);
|
||||
|
||||
ring.Beams.Clear();
|
||||
|
||||
if (ring.Disc == null) return;
|
||||
|
||||
uint discIndex = ring.Disc.Value;
|
||||
ring.Disc = null;
|
||||
|
||||
EntityManager.DestroyEntity(discIndex, 0f);
|
||||
}
|
||||
|
||||
private static string MakeKey(Skills skill, uint id)
|
||||
{
|
||||
return string.Create(CultureInfo.InvariantCulture, $"{skill}|{id}");
|
||||
}
|
||||
|
||||
private static Color GetSkillColor(Skills skill)
|
||||
{
|
||||
try
|
||||
{
|
||||
string hex = SkillsInfo.GetValue<string>(skill, "color");
|
||||
if (!string.IsNullOrEmpty(hex))
|
||||
return ColorTranslator.FromHtml(hex);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return Color.White;
|
||||
}
|
||||
|
||||
private static void SpawnDisc(Vector center, float radius, Ring ring)
|
||||
{
|
||||
var disc = EntityManager.CreateTrackedPropOverride(EntityManager.SystemOwnerIndex);
|
||||
if (disc == null || !disc.IsValid) return;
|
||||
|
||||
uint discIndex = disc.Index;
|
||||
ring.Disc = discIndex;
|
||||
|
||||
float scale = radius / discBaseRadius;
|
||||
Vector pos = new(center.X, center.Y, center.Z + 1f);
|
||||
|
||||
Server.NextFrame(() =>
|
||||
{
|
||||
var prop = Utilities.GetEntityFromIndex<CDynamicProp>((int)discIndex);
|
||||
if (prop == null || !prop.IsValid) return;
|
||||
|
||||
var node = prop.CBodyComponent?.SceneNode?.Owner?.Entity;
|
||||
if (node != null)
|
||||
node.Flags = (uint)(node.Flags & ~(1 << 2));
|
||||
|
||||
MakeNonSolid(prop);
|
||||
|
||||
prop.SetModel(discModel);
|
||||
prop.Teleport(pos, new QAngle(0, 0, 0));
|
||||
prop.DispatchSpawn();
|
||||
|
||||
prop.Render = Color.FromArgb(discAlpha, 255, 255, 255);
|
||||
Utilities.SetStateChanged(prop, "CBaseModelEntity", "m_clrRender");
|
||||
|
||||
var skeleton = prop.CBodyComponent?.SceneNode?.GetSkeletonInstance();
|
||||
if (skeleton != null)
|
||||
{
|
||||
skeleton.Scale = scale;
|
||||
prop.AcceptInput("SetScale", null, null, scale.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
Utilities.SetStateChanged(prop, "CBaseEntity", "m_CBodyComponent");
|
||||
});
|
||||
}
|
||||
|
||||
private static void MakeNonSolid(CDynamicProp prop)
|
||||
{
|
||||
var collision = prop.Collision;
|
||||
if (collision == null) return;
|
||||
|
||||
collision.SolidType = SolidType_t.SOLID_NONE;
|
||||
collision.SolidFlags = 12;
|
||||
|
||||
var attribute = collision.CollisionAttribute;
|
||||
if (attribute == null) return;
|
||||
|
||||
attribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_NEVER;
|
||||
attribute.CollisionFunctionMask = 0;
|
||||
attribute.InteractsAs = 0;
|
||||
attribute.InteractsWith = 0;
|
||||
attribute.InteractsExclude = 0;
|
||||
}
|
||||
|
||||
private static void SpawnRing(Vector center, float radius, int segments, Color color, Ring ring)
|
||||
{
|
||||
if (segments < 3 || radius <= 0) return;
|
||||
|
||||
float step = MathF.Tau / segments;
|
||||
Vector previous = PointOn(center, radius, 0);
|
||||
|
||||
for (int i = 1; i <= segments; i++)
|
||||
{
|
||||
Vector next = PointOn(center, radius, step * i);
|
||||
|
||||
var beam = EntityManager.CreateTrackedBeam(EntityManager.SystemOwnerIndex, previous, next, color);
|
||||
if (beam != null && beam.IsValid)
|
||||
{
|
||||
beam.Width = 1.2f;
|
||||
beam.EndWidth = 1.2f;
|
||||
|
||||
Utilities.SetStateChanged(beam, "CBeam", "m_fWidth");
|
||||
Utilities.SetStateChanged(beam, "CBeam", "m_fEndWidth");
|
||||
|
||||
ring.Beams.Add(beam.Index);
|
||||
}
|
||||
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector PointOn(Vector center, float radius, float angle)
|
||||
{
|
||||
return new Vector(
|
||||
center.X + MathF.Cos(angle) * radius,
|
||||
center.Y + MathF.Sin(angle) * radius,
|
||||
center.Z + ringHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -273,6 +273,40 @@ namespace src.utils
|
|||
return false;
|
||||
}
|
||||
|
||||
public static bool DestroyBeam(uint entityIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var beam = Utilities.GetEntityFromIndex<CBeam>((int)entityIndex);
|
||||
if (beam != null && beam.IsValid)
|
||||
{
|
||||
beam.Width = 0;
|
||||
beam.EndWidth = 0;
|
||||
beam.Render = Color.FromArgb(0, 0, 0, 0);
|
||||
|
||||
Utilities.SetStateChanged(beam, "CBeam", "m_fWidth");
|
||||
Utilities.SetStateChanged(beam, "CBeam", "m_fEndWidth");
|
||||
Utilities.SetStateChanged(beam, "CBaseModelEntity", "m_clrRender");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.PrintToConsole($"[EntityManager] DestroyBeam {entityIndex}: {ex.Message}");
|
||||
}
|
||||
|
||||
bool killed = DestroyEntity(entityIndex);
|
||||
|
||||
if (!SuppressKills)
|
||||
Server.NextFrame(() =>
|
||||
{
|
||||
var leftover = Utilities.GetEntityFromIndex<CBeam>((int)entityIndex);
|
||||
if (leftover != null && leftover.IsValid)
|
||||
leftover.Remove();
|
||||
});
|
||||
|
||||
return killed;
|
||||
}
|
||||
|
||||
public static bool DestroyEntity<T>(uint? entityIndex) where T : CBaseEntity
|
||||
{
|
||||
if (entityIndex == null) return false;
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ namespace jRandomSkills.src.utils
|
|||
"weapon_sg556" => KillfeedIcons.Sg556,
|
||||
"weapon_famas" => KillfeedIcons.Famas,
|
||||
"weapon_galilar" => KillfeedIcons.Galilar,
|
||||
"weapon_m4a4" => KillfeedIcons.M4A4,
|
||||
"weapon_m4a1" => KillfeedIcons.M4A4,
|
||||
"weapon_m4a1_silencer" => KillfeedIcons.M4A1,
|
||||
"weapon_ak47" => KillfeedIcons.Ak47,
|
||||
"weapon_aug" => KillfeedIcons.Aug,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ namespace src.utils
|
|||
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)
|
||||
public static void PrintToChat(CCSPlayerController player, string? msg, string border = "tb", string? title = null, bool ignoreIlliterate = false)
|
||||
{
|
||||
if (!player.IsValid) return;
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ namespace src.utils
|
|||
char symbol = config.LineSymbol;
|
||||
if (string.IsNullOrEmpty(title)) title = player.GetTranslation("jRandomSkills");
|
||||
|
||||
if (Illiterate.CheckIlliterateSkill(player))
|
||||
if (!ignoreIlliterate && Illiterate.CheckIlliterateSkill(player))
|
||||
msg = Illiterate.GetRandomText(msg);
|
||||
|
||||
if (border.Contains('t') && config.LineShow)
|
||||
|
|
@ -60,11 +60,39 @@ namespace src.utils
|
|||
player.PrintToChat($" {MeansureString.GetTextDashed("", maxWidth, symbol, config.LineColor)}");
|
||||
}
|
||||
|
||||
public static void EmitSoundToPlayer(CCSPlayerController? listener, string soundEvent, float volume)
|
||||
{
|
||||
var target = PlayerManager.GetPlayerFromEvent(listener);
|
||||
if (target == null || !target.IsValid) return;
|
||||
|
||||
target.EmitSound(soundEvent, new RecipientFilter(target), volume);
|
||||
}
|
||||
|
||||
public static bool IsFreezeTime()
|
||||
{
|
||||
return jRandomSkills.Instance?.GameRules?.FreezePeriod == true;
|
||||
}
|
||||
|
||||
public static bool IsPistolRound()
|
||||
{
|
||||
var gameRules = jRandomSkills.Instance?.GameRules;
|
||||
if (gameRules == null) return false;
|
||||
|
||||
if (gameRules.TotalRoundsPlayed == 0 || gameRules.GameRestart) return true;
|
||||
|
||||
try
|
||||
{
|
||||
if (ConVar.Find("mp_halftime")?.GetPrimitiveValue<bool>() != true) return false;
|
||||
|
||||
int maxRounds = ConVar.Find("mp_maxrounds")?.GetPrimitiveValue<int>() ?? 0;
|
||||
return maxRounds > 0 && maxRounds / 2 == gameRules.TotalRoundsPlayed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsHudFrame() => Server.TickCount % 4 == 0;
|
||||
|
||||
public static void RegisterSkill(Skills skill, string color, bool display = true)
|
||||
|
|
@ -396,7 +424,8 @@ namespace src.utils
|
|||
Skills.Deaf, Skills.ExpensiveAmmo, Skills.Giant, Skills.Glitch,
|
||||
Skills.Jammer, Skills.JumpBan, Skills.JumpCurse, Skills.LifeSwap,
|
||||
Skills.Magnifier, Skills.MoneySwap, Skills.Nightmare, Skills.Poison,
|
||||
Skills.JetKick, Skills.PrimaryBan, Skills.Thief, Skills.WildThrow
|
||||
Skills.JetKick, Skills.PrimaryBan, Skills.Thief, Skills.WildThrow,
|
||||
Skills.Voodoo, Skills.Nemesis, Skills.Bounty
|
||||
];
|
||||
|
||||
private static readonly HashSet<string> curseSkillNames = new(curseSkills.Select(s => s.ToString()), StringComparer.Ordinal);
|
||||
|
|
@ -490,6 +519,73 @@ namespace src.utils
|
|||
else curseCounts[victimIndex] = used - 1;
|
||||
}
|
||||
|
||||
private const uint crosshairBit = 1u << 8;
|
||||
|
||||
private static readonly object hudSuppressionLock = new();
|
||||
private static readonly Dictionary<uint, HashSet<string>> crosshairOwners = [];
|
||||
private static readonly Dictionary<uint, HashSet<string>> radarOwners = [];
|
||||
|
||||
public static void SetCrosshairHidden(CCSPlayerController? player, string owner, bool hide)
|
||||
{
|
||||
if (player == null || !player.IsValid) return;
|
||||
|
||||
var pawn = player.PlayerPawn?.Value;
|
||||
if (pawn == null || !pawn.IsValid) return;
|
||||
|
||||
if (!TryUpdateOwners(crosshairOwners, player.Index, owner, hide, out bool suppressed)) return;
|
||||
|
||||
if (suppressed) pawn.HideHUD |= crosshairBit;
|
||||
else pawn.HideHUD &= ~crosshairBit;
|
||||
|
||||
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_iHideHUD");
|
||||
}
|
||||
|
||||
public static void SetRadarDisabled(CCSPlayerController? player, string owner, bool disable)
|
||||
{
|
||||
if (player == null || !player.IsValid) return;
|
||||
if (!TryUpdateOwners(radarOwners, player.Index, owner, disable, out bool suppressed)) return;
|
||||
|
||||
player.ReplicateConVar("sv_disable_radar", suppressed ? "1" : "0");
|
||||
}
|
||||
|
||||
public static void ClearHudSuppression(uint playerIndex)
|
||||
{
|
||||
lock (hudSuppressionLock)
|
||||
{
|
||||
crosshairOwners.Remove(playerIndex);
|
||||
radarOwners.Remove(playerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearAllHudSuppression()
|
||||
{
|
||||
lock (hudSuppressionLock)
|
||||
{
|
||||
crosshairOwners.Clear();
|
||||
radarOwners.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryUpdateOwners(Dictionary<uint, HashSet<string>> registry, uint playerIndex, string owner, bool claim, out bool suppressed)
|
||||
{
|
||||
lock (hudSuppressionLock)
|
||||
{
|
||||
if (!registry.TryGetValue(playerIndex, out var owners))
|
||||
{
|
||||
owners = [];
|
||||
registry[playerIndex] = owners;
|
||||
}
|
||||
|
||||
bool before = owners.Count > 0;
|
||||
bool changed = claim ? owners.Add(owner) : owners.Remove(owner);
|
||||
|
||||
suppressed = owners.Count > 0;
|
||||
if (!suppressed) registry.Remove(playerIndex);
|
||||
|
||||
return changed && before != suppressed;
|
||||
}
|
||||
}
|
||||
|
||||
public static CCSPlayerController[] GetSelectableEnemies(CCSPlayerController player, bool respectCurseLimit = false)
|
||||
{
|
||||
if (player == null || !player.IsValid) return [];
|
||||
|
|
@ -575,6 +671,26 @@ namespace src.utils
|
|||
return true;
|
||||
}
|
||||
|
||||
public readonly record struct HiddenPawn(uint Index, CsTeam Team, CCSPlayerPawn Pawn, bool HoldsBomb);
|
||||
|
||||
public static List<HiddenPawn> ResolveHiddenPawns(ICollection<uint> playerIndexes, uint? bombOwnerIndex)
|
||||
{
|
||||
List<HiddenPawn> hidden = new(playerIndexes.Count);
|
||||
|
||||
foreach (var playerIndex in playerIndexes)
|
||||
{
|
||||
var controller = PlayerManager.GetPlayerEvent(Utilities.GetPlayerFromIndex((int)playerIndex));
|
||||
if (controller == null || !controller.IsValid) continue;
|
||||
|
||||
var pawn = controller.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) continue;
|
||||
|
||||
hidden.Add(new HiddenPawn(controller.Index, controller.Team, pawn, bombOwnerIndex == controller.Index));
|
||||
}
|
||||
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public static void HideCarriedEntities(CCheckTransmitInfo info, CCSPlayerPawn? pawn)
|
||||
{
|
||||
if (pawn == null || !pawn.IsValid) return;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace src.utils
|
|||
private static SkillsInfoModel? _indexedConfig;
|
||||
private static ConcurrentDictionary<string, DefaultSkillInfo> _byName = new();
|
||||
private static readonly ConcurrentDictionary<(Type Type, string Key), MemberInfo?> _memberCache = new();
|
||||
private static readonly ConcurrentDictionary<(DefaultSkillInfo Config, string Key, Type Target), object?> _valueCache = new();
|
||||
|
||||
public static SkillsInfoModel LoadSkillsInfo()
|
||||
{
|
||||
|
|
@ -92,6 +93,12 @@ namespace src.utils
|
|||
if (!_byName.TryGetValue(skill.ToString()!, out var skillConfig) || skillConfig == null)
|
||||
return default!;
|
||||
|
||||
object? cached = _valueCache.GetOrAdd((skillConfig, key, typeof(T)), k => Resolve(k.Config, k.Key, k.Target));
|
||||
return cached == null ? default! : (T)cached;
|
||||
}
|
||||
|
||||
private static object? Resolve(DefaultSkillInfo skillConfig, string key, Type targetType)
|
||||
{
|
||||
var member = _memberCache.GetOrAdd((skillConfig.GetType(), key), k =>
|
||||
{
|
||||
MemberInfo? m = k.Type.GetProperty(k.Key, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
|
|
@ -106,11 +113,10 @@ namespace src.utils
|
|||
_ => null
|
||||
};
|
||||
|
||||
if (value == null) return default!;
|
||||
if (value == null) return null;
|
||||
|
||||
Type targetType = typeof(T);
|
||||
Type? underlyingType = Nullable.GetUnderlyingType(targetType);
|
||||
return (T)Convert.ChangeType(value, underlyingType ?? targetType);
|
||||
return Convert.ChangeType(value, underlyingType ?? targetType);
|
||||
}
|
||||
|
||||
private static void EnsureIndex()
|
||||
|
|
@ -124,6 +130,7 @@ namespace src.utils
|
|||
_byName = dict;
|
||||
_indexedConfig = config;
|
||||
_memberCache.Clear();
|
||||
_valueCache.Clear();
|
||||
}
|
||||
|
||||
public class SkillsInfoModel : ConcurrentBag<DefaultSkillInfo>
|
||||
|
|
@ -147,10 +154,11 @@ namespace src.utils
|
|||
}
|
||||
}
|
||||
|
||||
public class DefaultSkillInfo(Skills skill, bool active = true, string color = "#ffffff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common)
|
||||
public class DefaultSkillInfo(Skills skill, bool active = true, string color = "#ffffff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, bool disableOnPistolRound = false)
|
||||
{
|
||||
public bool NeedsTeammates { get; set; } = needsTeammates;
|
||||
public bool DisableOnFreezeTime { get; set; } = disableOnFreezeTime;
|
||||
public bool DisableOnPistolRound { get; set; } = disableOnPistolRound;
|
||||
public int OnlyTeam { get; set; } = (int)onlyTeam;
|
||||
public string Color { get; set; } = color;
|
||||
public bool Active { get; set; } = active;
|
||||
|
|
|
|||
84
jRandomSkills - SRC Files/src/utils/WeaponPool.cs
Normal file
84
jRandomSkills - SRC Files/src/utils/WeaponPool.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
namespace src.utils
|
||||
{
|
||||
public static class WeaponPool
|
||||
{
|
||||
private static readonly object buildLock = new();
|
||||
private static bool built;
|
||||
|
||||
private static HashSet<string> rifles = new(StringComparer.Ordinal);
|
||||
private static HashSet<string> pistols = new(StringComparer.Ordinal);
|
||||
private static HashSet<string> grenades = new(StringComparer.Ordinal);
|
||||
private static HashSet<string> all = new(StringComparer.Ordinal);
|
||||
|
||||
private static string[] rifleArray = [];
|
||||
private static string[] pistolArray = [];
|
||||
private static string[] grenadeArray = [];
|
||||
private static string[] allArray = [];
|
||||
|
||||
public static HashSet<string> Rifles { get { Build(); return rifles; } }
|
||||
public static HashSet<string> Pistols { get { Build(); return pistols; } }
|
||||
public static HashSet<string> Grenades { get { Build(); return grenades; } }
|
||||
public static HashSet<string> All { get { Build(); return all; } }
|
||||
|
||||
public static string[] RifleList { get { Build(); return rifleArray; } }
|
||||
public static string[] PistolList { get { Build(); return pistolArray; } }
|
||||
public static string[] GrenadeList { get { Build(); return grenadeArray; } }
|
||||
public static string[] AllList { get { Build(); return allArray; } }
|
||||
|
||||
public static bool IsRifle(string? weapon) => weapon != null && Rifles.Contains(Normalize(weapon));
|
||||
public static bool IsPistol(string? weapon) => weapon != null && Pistols.Contains(Normalize(weapon));
|
||||
public static bool IsGrenade(string? weapon) => weapon != null && Grenades.Contains(Normalize(weapon));
|
||||
public static bool IsWeapon(string? weapon) => weapon != null && All.Contains(Normalize(weapon));
|
||||
|
||||
public static string Normalize(string weapon)
|
||||
{
|
||||
if (string.IsNullOrEmpty(weapon)) return string.Empty;
|
||||
return weapon.StartsWith("weapon_", StringComparison.Ordinal) ? weapon : "weapon_" + weapon;
|
||||
}
|
||||
|
||||
public static void Invalidate()
|
||||
{
|
||||
lock (buildLock) built = false;
|
||||
}
|
||||
|
||||
private static void Build()
|
||||
{
|
||||
if (built) return;
|
||||
|
||||
lock (buildLock)
|
||||
{
|
||||
if (built) return;
|
||||
|
||||
var pools = Config.LoadedConfig?.Weapons;
|
||||
|
||||
rifles = Collect(pools?.Rifle);
|
||||
pistols = Collect(pools?.Pistol);
|
||||
grenades = Collect(pools?.Grenade);
|
||||
|
||||
all = new HashSet<string>(rifles, StringComparer.Ordinal);
|
||||
all.UnionWith(pistols);
|
||||
|
||||
rifleArray = [.. rifles];
|
||||
pistolArray = [.. pistols];
|
||||
grenadeArray = [.. grenades];
|
||||
allArray = [.. all];
|
||||
|
||||
built = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> Collect(List<string>? source)
|
||||
{
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (source == null) return set;
|
||||
|
||||
foreach (var entry in source)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry)) continue;
|
||||
set.Add(Normalize(entry.Trim()));
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue