initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
111
Outnumbered/Players.cs
Normal file
111
Outnumbered/Players.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Outnumbered.Data;
|
||||
|
||||
namespace Outnumbered;
|
||||
|
||||
// The one front door from a controller/slot to PlayerData, plus the shared player predicates/enumerators and the
|
||||
// slot-deferral helper. Handlers route through these so the engine's controller/SteamID surface (the part most likely
|
||||
// to shift across CSSharp builds) and the slot-reuse-after-NextFrame contract live in one place. Deliberate exceptions:
|
||||
// the hot OnEntityTakeDamagePre resolves inline (perf), and a few sites keep the raw SteamID where it's the actual key
|
||||
// (the _hudOff/_dmgReadout HashSets, LoadPlayer, identity re-validation) rather than a PlayerData lookup.
|
||||
public sealed partial class OutnumberedPlugin
|
||||
{
|
||||
// The canonical controller -> PlayerData lookup (by SteamID). Every other resolver routes through it.
|
||||
internal PlayerData? PdOf(CCSPlayerController p)
|
||||
{
|
||||
var sid = p.AuthorizedSteamID?.SteamId64;
|
||||
return sid is not null && _players.TryGetValue(sid.Value, out var pd) ? pd : null;
|
||||
}
|
||||
|
||||
internal PlayerData? PdOf(int slot)
|
||||
{
|
||||
var p = Utilities.GetPlayerFromSlot(slot);
|
||||
return p is { IsValid: true } ? PdOf(p) : null;
|
||||
}
|
||||
|
||||
// [NotNullWhen(true)] lets `if (!IsHuman(p)) return;` flow non-null through to the body.
|
||||
// Resolve a pawn back to its owning controller (the schema dance the damage hook does for attacker + victim).
|
||||
internal static CCSPlayerController? ControllerOfPawn(CCSPlayerPawn? pawn) => pawn?.Controller.Value?.As<CCSPlayerController>();
|
||||
|
||||
internal static bool IsHuman([NotNullWhen(true)] CCSPlayerController? p) => p is { IsValid: true, IsBot: false, IsHLTV: false };
|
||||
internal static bool IsLiveHuman([NotNullWhen(true)] CCSPlayerController? p) => p is { IsValid: true, IsBot: false, IsHLTV: false } && p.PawnIsAlive;
|
||||
// Bot-side mirror. IsHLTV:false excludes a SourceTV/GOTV proxy (the engine marks it IsBot too) — so a relay can never
|
||||
// be treated as a combat bot.
|
||||
internal static bool IsBot([NotNullWhen(true)] CCSPlayerController? p) => p is { IsValid: true, IsBot: true, IsHLTV: false };
|
||||
internal static bool IsLiveBot([NotNullWhen(true)] CCSPlayerController? p) => p is { IsValid: true, IsBot: true, IsHLTV: false } && p.PawnIsAlive;
|
||||
|
||||
// Per-tick callers iterate Utilities.GetPlayers() with the IsHuman/IsLiveHuman/IsBot predicates inline (no Where-iterator
|
||||
// alloc); Humans()/Bots() are the allocating convenience for cold paths (e.g. the hot-reload seed, the bot pool walk).
|
||||
internal static IEnumerable<CCSPlayerController> Humans() => Utilities.GetPlayers().Where(IsHuman);
|
||||
internal static IEnumerable<CCSPlayerController> Bots() => Utilities.GetPlayers().Where(IsBot);
|
||||
|
||||
// Force every CT-side bot back to T (bot_join_team only affects NEWLY-added bots). Shared by the steady-state SyncBots
|
||||
// and survival's ManageBots; the caller passes its already-materialized roster so there's no extra GetPlayers() walk.
|
||||
internal static void ForceBotsToTerrorist(IEnumerable<CCSPlayerController> players)
|
||||
{
|
||||
foreach (var b in players)
|
||||
if (IsBot(b) && b.Team == CsTeam.CounterTerrorist) b.SwitchTeam(CsTeam.Terrorist);
|
||||
}
|
||||
|
||||
// Run `body` next frame for the player still occupying `slot`, re-resolved and re-validated (the slot may have been
|
||||
// reused). The overload also pins the SteamID so a within-frame disconnect+reuse by a different person can't inherit.
|
||||
internal static void NextFrameForSlot(int slot, Action<CCSPlayerController> body, bool requireAlive = false) =>
|
||||
Server.NextFrame(() =>
|
||||
{
|
||||
var p = Utilities.GetPlayerFromSlot(slot);
|
||||
if (p is { IsValid: true } && (!requireAlive || p.PawnIsAlive)) body(p);
|
||||
});
|
||||
|
||||
internal static void NextFrameForSlot(int slot, ulong expectSid, Action<CCSPlayerController> body, bool requireAlive = false) =>
|
||||
Server.NextFrame(() =>
|
||||
{
|
||||
var p = Utilities.GetPlayerFromSlot(slot);
|
||||
if (p is { IsValid: true } && p.AuthorizedSteamID?.SteamId64 == expectSid && (!requireAlive || p.PawnIsAlive)) body(p);
|
||||
});
|
||||
|
||||
// Defer a HP/armor cap re-apply (optionally re-locking the default loadout) to next frame — re-resolved by slot and
|
||||
// SteamID-pinned so a within-frame disconnect+slot-reuse can't apply this player's caps to whoever inherits the slot.
|
||||
// No-op if dead now / no SteamID / not alive next frame. Used after a stat buy / prestige reset / admin grant.
|
||||
internal void DeferReapplyCaps(CCSPlayerController p, bool relockLoadout = false)
|
||||
{
|
||||
if (!p.PawnIsAlive || p.AuthorizedSteamID?.SteamId64 is not { } sid) return;
|
||||
NextFrameForSlot(p.Slot, sid, pl =>
|
||||
{
|
||||
if (PdOf(pl) is not { } pd) return;
|
||||
if (relockLoadout) ApplyLoadout(pl, false);
|
||||
ApplyMaxHpArmor(pl, pd);
|
||||
}, requireAlive: true);
|
||||
}
|
||||
|
||||
// Reap entries keyed by player slot whose controller has vanished WITHOUT a clean disconnect (map change /
|
||||
// bot-replaces-human / kick race). Collect-then-invoke because onOrphan typically mutates `map`. The caller owns the
|
||||
// reused `scratch` list and passes the concrete Dictionary (struct enumerator) + a CACHED onOrphan delegate, so the
|
||||
// per-tick walk allocates nothing. Used by the Hud (world-text) and Shop (session) per-tick reaps.
|
||||
internal static void ReapOrphanSlots<T>(Dictionary<int, T> map, List<int> scratch, Action<int> onOrphan)
|
||||
{
|
||||
scratch.Clear();
|
||||
foreach (var slot in map.Keys)
|
||||
{
|
||||
var pl = Utilities.GetPlayerFromSlot(slot);
|
||||
if (pl is null || !pl.IsValid || pl.IsBot) scratch.Add(slot);
|
||||
}
|
||||
foreach (var slot in scratch) onOrphan(slot);
|
||||
}
|
||||
|
||||
// CheckTransmit shared shape: a per-player world-text panel keyed by slot must be hidden from EVERY OTHER player (each
|
||||
// sees only their own). The Hud + Shop transmit handlers differ only in which entities a slot owns — pass a STATIC
|
||||
// `remove` (no captures, so it's cached, no per-call alloc on this hot listener) that strips one slot's entities.
|
||||
internal static void HideForeignSlotEntities<T>(CCheckTransmitInfoList infoList, Dictionary<int, T> map, Action<CCheckTransmitInfo, T> remove)
|
||||
{
|
||||
if (map.Count == 0) return;
|
||||
foreach (var (info, receiver) in infoList)
|
||||
{
|
||||
if (receiver is null) continue;
|
||||
foreach (var (slot, entry) in map)
|
||||
if (slot != receiver.Slot) remove(info, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue