VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
Visibility.cpp
Go to the documentation of this file.
6#include <algorithm>
7#include <checktransmitinfo.h>
8#include <cstdint>
9#include <entity2/entityinstance.h>
10#include <entityhandle.h>
11#include <tier1/utlvector.h>
12#include <utility>
13
14namespace VoltMod
15{
16
17static constexpr int MaxIndicesPerPlayer = 24;
18
19// The schema's CNetworkUtlVectorBase<CHandle<T>> fields are 24 bytes, laid out as a CUtlVector.
21static_assert(sizeof(HandleVector) == 24);
22
24{
25 int Slot = -1;
27 CEntityInstance* Pawn = nullptr;
28 int IndexCount = 0;
29 std::array<int, MaxIndicesPerPlayer> PawnIndices{}; // pawn, weapons and wearables
30};
31
32static void AddIndex(HiddenPlayer& player, int index)
33{
34 if (index > 0 && player.IndexCount < MaxIndicesPerPlayer)
35 {
36 player.PawnIndices[player.IndexCount++] = index;
37 }
38}
39
41{
42 const auto* handles = static_cast<const HandleVector*>(vector);
43 if (!handles)
44 {
45 return;
46 }
47 for (int i = 0; i < handles->Count() && i < MaxIndicesPerPlayer; ++i)
48 {
49 AddIndex(player, entities.Get(EntityRef{static_cast<uint32_t>(handles->Element(i).ToInt())}).Index());
50 }
51}
52
53// The pawn `recipientSlot` spectates, which stays transmitted.
54static CEntityInstance* ObserverTarget(EntitySystem& entities, int recipientSlot)
55{
56 // The observer pawn carries the camera while dead or spectating.
58 const Schema::CPlayer_ObserverServices services = pawn.ObserverServices();
59 if (!services)
60 {
61 return nullptr;
62 }
63
64 return entities.Get(services.ObserverTarget()).Raw();
65}
66
69{
70 out.Slot = slot;
71
73 if (!controller)
74 {
75 return;
76 }
77
79 {
80 out.ControllerIndex = controller.Index();
81 }
82
83 if (!pawnHidden)
84 {
85 return;
86 }
87
88 Pawn pawn = controller.Pawn();
89 if (!pawn)
90 {
91 return;
92 }
93
94 out.Pawn = pawn.Raw();
95 AddIndex(out, pawn.Index());
96
97 AddHandleVector(entities, out, pawn.WeaponServices().MyWeapons());
98 AddHandleVector(entities, out, pawn.MyWearables());
99}
100
102 : _entities(entities), _bindings(bindings)
103{
104 _slotListener = slots.Changed += [this](int slot) {
105 if (!IsValidSlot(slot))
106 {
107 return;
108 }
109 _state[slot] = {};
110 std::erase_if(_private, [slot](const PrivateEntity& e) { return e.Viewer == slot; });
111 };
112}
113
115{
116 if (IsValidSlot(slot))
117 {
118 _state[slot].PawnHidden = hidden;
119 }
120}
121
123{
124 if (IsValidSlot(slot))
125 {
126 _state[slot].ControllerHidden = hidden;
127 }
128}
129
131{
132 return IsValidSlot(slot) && _state[slot].PawnHidden;
133}
134
136{
137 if (!_bindings.VisibilityRecipientSlot)
138 {
139 return std::unexpected(Error::Unsupported("the CheckTransmitPlayerSlot offset did not bind"));
140 }
141 return {};
142}
143
145{
146 if (!entity || !IsValidSlot(slot))
147 {
148 return;
149 }
150
151 SetPrivate({.Entity = entity, .Viewer = slot});
152}
153
155{
156 if (!entity || !IsPlaying(team))
157 {
158 return;
159 }
160
161 SetPrivate({.Entity = entity, .ShownTo = team});
162}
163
165{
166 if (!entity || !IsPlaying(team))
167 {
168 return;
169 }
170
171 SetPrivate({.Entity = entity, .HiddenFrom = team});
172}
173
174void Visibility::SetPrivate(const PrivateEntity& entry)
175{
176 const auto found = std::ranges::find(_private, entry.Entity, &PrivateEntity::Entity);
177 if (found != _private.end())
178 {
179 *found = entry;
180 }
181 else
182 {
183 _private.push_back(entry);
184 }
185}
186
188{
189 std::erase_if(_private, [entity](const PrivateEntity& e) { return e.Entity == entity; });
190}
191
192std::shared_ptr<GlowVision> Visibility::CreateGlow(int viewerSlot, GlowConfig config)
193{
194 return std::make_shared<GlowVision>(_entities, *this, viewerSlot, std::move(config));
195}
196
198{
199 return std::make_shared<GlowVision>(_entities, *this, viewerTeam, std::move(config));
200}
201
202bool Visibility::PrivateEntity::HiddenFor(int recipient, Team recipientTeam) const
203{
204 if (Viewer >= 0)
205 {
206 return Viewer != recipient;
207 }
208 if (ShownTo != Team::None)
209 {
210 return ShownTo != recipientTeam;
211 }
212 return HiddenFrom == recipientTeam;
213}
214
216{
217 // Even unbound: indices are recycled, and the list must not grow.
218 for (auto& entry : _private)
219 {
220 const Entity entity = _entities.Get(entry.Entity);
221 entry.Index = entity ? entity.Index() : -1;
222 }
223 std::erase_if(_private, [](const PrivateEntity& e) { return e.Index <= 0; });
224
225 if (!_bindings.VisibilityRecipientSlot || !infoList)
226 {
227 return;
228 }
229 if (_private.empty() && std::ranges::none_of(_state, &SlotState::Any))
230 {
231 return;
232 }
233
234 // Shared by every recipient; only the self and spectator exemptions vary.
235 std::array<HiddenPlayer, MaxPlayers> hidden;
236 int hiddenCount = 0;
237 for (int slot = 0; slot < MaxPlayers; ++slot)
238 {
239 const auto& state = _state[slot];
240 if (state.Any())
241 {
242 CollectHiddenPlayer(_entities, slot, state.PawnHidden, state.ControllerHidden, hidden[hiddenCount++]);
243 }
244 }
245
246 for (int i = 0; i < infoCount; ++i)
247 {
248 auto* info = infoList[i];
249 if (!info || !info->m_pTransmitEntity)
250 {
251 continue;
252 }
253
254 const int recipient = static_cast<int>(_bindings.VisibilityRecipientSlot.Read(info));
255 CEntityInstance* observed = hiddenCount > 0 ? ObserverTarget(_entities, recipient) : nullptr;
256 const Team recipientTeam = _entities.Controller(recipient).TeamNum();
257
258 for (int h = 0; h < hiddenCount; ++h)
259 {
260 const auto& player = hidden[h];
261 if (player.Slot == recipient)
262 {
263 continue;
264 }
265
266 if (observed != player.Pawn)
267 {
268 for (int n = 0; n < player.IndexCount; ++n)
269 {
270 info->m_pTransmitEntity->Clear(player.PawnIndices[n]);
271 }
272 }
273
274 if (player.ControllerIndex > 0)
275 {
276 info->m_pTransmitEntity->Clear(player.ControllerIndex);
277 }
278 }
279
280 for (const auto& entry : _private)
281 {
282 if (entry.HiddenFor(recipient, recipientTeam))
283 {
284 info->m_pTransmitEntity->Clear(entry.Index);
285 }
286 }
287 }
288}
289
290} // namespace VoltMod
A player's identity behind a slot: name, money, team, scoreboard row. It survives death and respawn; ...
VoltMod::Pawn InputPawn() const
runtime.Entities: finds and creates entities. Everything it returns is valid for this frame only; see...
VoltMod::Controller Controller(int slot)
Entity Get(EntityRef ref)
A live entity, valid for this frame only.
Definition Entity.hpp:29
int Index() const
Definition Entity.cpp:77
CEntityInstance * Raw() const noexcept
Definition Entity.hpp:47
T Read(const void *base) const
A player's body: health, armor, movement, aim, weapons. The engine replaces it on every spawn; the Co...
Definition Pawn.hpp:19
VoltMod::Controller Controller() const
Definition Pawn.cpp:213
"The occupant of this slot changed" - raised by PlayerManager, consumed below it.
void ShowOnlyToTeam(EntityRef entity, Team team)
Status Available() const
Visibility(EntitySystem &entities, const Bindings &bindings, SlotEvents &slots)
void SetPawnHidden(int slot, bool hidden)
bool IsPawnHidden(int slot) const
void HideFromTeam(EntityRef entity, Team team)
void ShowToEveryone(EntityRef entity)
void SetControllerHidden(int slot, bool hidden)
std::shared_ptr< GlowVision > CreateGlow(int viewerSlot, GlowConfig config={})
void ShowOnlyTo(EntityRef entity, int slot)
void OnCheckTransmit(CCheckTransmitInfo **infoList, int infoCount)
constexpr bool IsPlaying(Team team)
Definition Team.hpp:18
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static void CollectHiddenPlayer(EntitySystem &entities, int slot, bool pawnHidden, bool controllerHidden, HiddenPlayer &out)
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
CUtlVector< CEntityHandle > HandleVector
static CEntityInstance * ObserverTarget(EntitySystem &entities, int recipientSlot)
std::expected< void, Error > Status
Definition Result.hpp:67
static void AddHandleVector(EntitySystem &entities, HiddenPlayer &player, const void *vector)
constexpr int MaxPlayers
Definition Slot.hpp:7
static constexpr int MaxIndicesPerPlayer
static void AddIndex(HiddenPlayer &player, int index)
Offset< uint8_t > VisibilityRecipientSlot
Definition Bindings.hpp:96
static Error Unsupported(std::string detail)
Definition Result.hpp:57
std::array< int, MaxIndicesPerPlayer > PawnIndices