VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
ScreenEntity.cpp
Go to the documentation of this file.
1#include "Ui/ScreenEntity.hpp"
2
8#include <format>
9#include <tier1/utlstring.h>
10#include <tier1/utlvector.h>
11#include <utility>
12
13namespace VoltMod
14{
15
17
18/** EHudPanelClassStatus_t values. */
19static constexpr int32_t ClassAbsent = 0;
20static constexpr int32_t ClassPresent = 1;
21
22/** CS2 refuses to intern past this many entries per table; stop short so a runaway caller cannot fill them. */
23static constexpr int StringTableCap = 1024;
24static constexpr int StringTableHeadroom = 64;
25
26/** Assigned in place: CUtlString allocates inside tier0, the allocator the engine frees with. */
27static void SetStr(CUtlString& out, std::string_view text)
28{
29 out.SetDirect(text.data(), static_cast<int>(text.size()));
30}
31
33 int owner)
34 : _entities(entities), _visibility(visibility), _layout(std::move(layout)), _owner(owner)
35{
36 _slotChanges = slots.Changed += [this](int slot) {
37 _playersChangedSinceSpawn = true;
38 if (slot == _owner) // a player screen leaves with its owner
39 {
40 Remove();
41 }
42 else
43 {
44 _written.RemoveSlot(slot);
45 }
46 };
47 // The game resets the slot's per-player state as its client finishes loading, so resend it.
48 _fullConnects = slots.FullyConnected += [this](int slot) {
49 _playersChangedSinceSpawn = true;
50 _written.RemoveSlot(slot);
51 };
52}
53
58
60{
61 return static_cast<bool>(_entities.Get(_entity));
62}
63
65{
67 {
68 return false;
69 }
70
71 const auto target = CacheSlotFor(slot);
72 if (!target)
73 {
74 return false;
75 }
76
77 if (!Exists() && !SpawnOrWarn())
78 {
79 return false;
80 }
81
82 if (*target == EveryoneSlot || Covers(*target))
83 {
84 return true;
85 }
86
87 // Respawning for any other reason would retry a hopeless spawn every frame.
88 if (!_playersChangedSinceSpawn)
89 {
90 return false;
91 }
92
93 return SpawnOrWarn() && Covers(*target);
94}
95
97{
98 if (IsForPlayer())
99 {
100 _visibility.ShowToEveryone(_entity);
101 }
102 _entities.Get(_entity).Remove();
103
104 _entity = {};
105 _written.Clear();
106}
107
108void ScreenEntity::WriteText(int slot, std::string_view variable, std::string_view value)
109{
110 const auto target = CacheSlotFor(slot);
111 // One entity holds one layout, so the variable alone names the text.
112 if (!target || !_written.Changed(*target, WriteKind::Text, {}, variable, value))
113 {
114 return;
115 }
116
117 CheckWrite(*target, SendText(ContentSlotFor(*target), variable, value), variable);
118}
119
120void ScreenEntity::WriteClass(int slot, std::string_view elementId, std::string_view className, bool on)
121{
122 const auto target = CacheSlotFor(slot);
123 if (!target || !_written.Changed(*target, WriteKind::Class, elementId, className, on ? "1" : "0"))
124 {
125 return;
126 }
127
128 CheckWrite(*target, SendClass(ContentSlotFor(*target), elementId, className, on), elementId);
129}
130
132{
133 const auto target = CacheSlotFor(slot);
134 if (!target || !_written.CursorChanged(*target, shown))
135 {
136 return;
137 }
138
139 CheckWrite(*target, SendCursor(*target, shown), "the cursor");
140}
141
142Result<int> ScreenEntity::CacheSlotFor(int slot) const
143{
144 if (!IsForPlayer())
145 {
146 return slot;
147 }
148
149 if (slot != EveryoneSlot && slot != _owner)
150 {
151 return std::unexpected(Error::Invalid(std::format("this screen belongs to slot {}", _owner)));
152 }
153
154 return _owner;
155}
156
157int ScreenEntity::ContentSlotFor(int cacheSlot) const noexcept
158{
159 return IsForPlayer() ? EveryoneSlot : cacheSlot;
160}
161
162Status ScreenEntity::Spawn()
163{
164 _playersChangedSinceSpawn = false;
165 Remove();
166
167 if (Status available = _entities.Available(); !available)
168 {
169 return available;
170 }
171
172 KeyValues values;
173 values.Set("layout", _layout.Resource());
174
175 const Entity spawned = _entities.Spawn("custom_hud_layout", values);
176 if (!spawned)
177 {
178 return std::unexpected(Error::Engine("the engine refused to spawn custom_hud_layout"));
179 }
180
181 _entity = spawned.Ref();
182 if (IsForPlayer())
183 {
184 _visibility.ShowOnlyTo(_entity, _owner);
185 }
186 return {};
187}
188
189bool ScreenEntity::SpawnOrWarn()
190{
191 const Status spawned = Spawn();
192 if (!spawned)
193 {
194 Log::Warn("Screen '{}': spawn failed ({}).", _layout.Name(), spawned.error().Detail);
195 }
196 return spawned.has_value();
197}
198
199int ScreenEntity::PlayerStateCount() const
200{
201 // The embedded vector keeps its count first, which is what the engine itself reads before indexing.
202 const Schema::CCSCustomHudLayout layout{_entities.Get(_entity).Raw()};
203 return layout ? layout.PlayerLayoutStates() : -1;
204}
205
206bool ScreenEntity::Covers(int slot) const
207{
208 return IsValidSlot(slot) && PlayerStateCount() > slot;
209}
210
211Result<CEntityInstance*> ScreenEntity::EntityForWrite(int engineSlot) const
212{
213 Entity entity = _entities.Get(_entity);
214 if (!entity)
215 {
216 return std::unexpected(Error::NotFound("the custom_hud_layout entity no longer exists"));
217 }
218
219 const Schema::CCSCustomHudLayout layout{entity.Raw()};
220 for (const auto& [name, table] :
221 {std::pair{"element id", layout.PanelIds()}, std::pair{"class name", layout.ClassNames()},
222 std::pair{"text variable", layout.DialogVariableNames()}})
223 {
224 const int count = table ? static_cast<const StringTable*>(table)->Count() : -1;
226 {
227 return std::unexpected(
228 Error::Failed(std::format("the {} table is nearly full ({}/{})", name, count, StringTableCap)));
229 }
230 }
231
233 {
234 return entity.Raw();
235 }
236
237 // The engine's per-player setters return silently for a slot past the state count.
238 if (const int states = PlayerStateCount(); states <= engineSlot)
239 {
240 return std::unexpected(Error::Failed(
241 std::format("slot {} has no per-player layout state (the entity holds {})", engineSlot, states)));
242 }
243
244 return entity.Raw();
245}
246
247Status ScreenEntity::SendText(int engineSlot, std::string_view variable, std::string_view value)
248{
249 auto entity = EntityForWrite(engineSlot);
250 if (!entity)
251 {
252 return std::unexpected(entity.error());
253 }
254
255 CUtlString root, name, text;
256 SetStr(root, _layout.Name());
257 SetStr(name, variable);
258 SetStr(text, value);
259
260 const Bindings& bindings = _entities.Bindings();
262 {
263 if (!bindings.CustomHudSetDialogVariable)
264 {
265 return std::unexpected(Error::Unsupported("the custom HUD text setter did not bind"));
266 }
267 bindings.CustomHudSetDialogVariable(*entity, &root, &name, &text);
268 return {};
269 }
270
271 if (!bindings.CustomHudSetDialogVariableForPlayer)
272 {
273 return std::unexpected(Error::Unsupported("the custom HUD per-player text setter did not bind"));
274 }
275 bindings.CustomHudSetDialogVariableForPlayer(*entity, engineSlot, &root, &name, &text);
276 return {};
277}
278
279Status ScreenEntity::SendClass(int engineSlot, std::string_view elementId, std::string_view className, bool on)
280{
281 auto entity = EntityForWrite(engineSlot);
282 if (!entity)
283 {
284 return std::unexpected(entity.error());
285 }
286
287 CUtlString element, name;
289 SetStr(name, className);
291
292 const Bindings& bindings = _entities.Bindings();
294 {
295 if (!bindings.CustomHudSetHasClass)
296 {
297 return std::unexpected(Error::Unsupported("the custom HUD class setter did not bind"));
298 }
299 bindings.CustomHudSetHasClass(*entity, &element, &name, state);
300 return {};
301 }
302
303 if (!bindings.CustomHudSetHasClassForPlayer)
304 {
305 return std::unexpected(Error::Unsupported("the custom HUD per-player class setter did not bind"));
306 }
307 bindings.CustomHudSetHasClassForPlayer(*entity, engineSlot, &element, &name, state);
308 return {};
309}
310
311Status ScreenEntity::SendCursor(int engineSlot, bool shown)
312{
313 auto entity = EntityForWrite(engineSlot);
314 if (!entity)
315 {
316 return std::unexpected(entity.error());
317 }
318
320 {
321 const auto& set = _entities.Bindings().CustomHudSetInputCapture;
322 if (!set)
323 {
324 return std::unexpected(Error::Unsupported("the custom HUD input capture setter did not bind"));
325 }
327 return {};
328 }
329
330 // No engine setter takes the global state; this plain bool has no container or index behind it.
331 Schema::CCSCustomHudLayout{*entity}.GlobalLayoutState().SetInputCaptureEnabled(shown);
332 return {};
333}
334
335void ScreenEntity::CheckWrite(int cacheSlot, const Status& status, std::string_view what)
336{
338 {
339 return;
340 }
341
342 _written.Invalidate(cacheSlot);
343 if (_written.IsFirstFailure(cacheSlot))
344 {
345 Log::Warn("Screen '{}': writing {} for slot {} failed ({}).", _layout.Name(), what, cacheSlot,
346 status.error().Detail);
347 }
348}
349
350} // namespace VoltMod
runtime.Entities: finds and creates entities. Everything it returns is valid for this frame only; see...
const VoltMod::Bindings & Bindings() const noexcept
Entity Spawn(std::string_view className, KeyValues &values)
Entity Get(EntityRef ref)
void Remove() const
Definition Entity.cpp:165
CEntityInstance * Raw() const noexcept
Definition Entity.hpp:47
A layout resource the client will accept, checked before anything is spawned.
std::string_view Name() const noexcept
const std::string & Resource() const noexcept
void WriteText(int slot, std::string_view variable, std::string_view value)
void WriteClass(int slot, std::string_view elementId, std::string_view className, bool on)
bool EnsureSpawned(int slot)
ScreenEntity(EntitySystem &entities, SlotEvents &slots, Visibility &visibility, LayoutPath layout, int owner)
void WriteCursor(int slot, bool shown)
"The occupant of this slot changed" - raised by PlayerManager, consumed below it.
Which clients receive which entities, filtered after CheckTransmit.
void ShowToEveryone(EntityRef entity)
void ShowOnlyTo(EntityRef entity, int slot)
void Invalidate(int slot)
bool IsFirstFailure(int slot)
bool Changed(int slot, WriteKind kind, std::string_view elementId, std::string_view name, std::string_view value)
bool CursorChanged(int slot, bool shown)
void RemoveSlot(int slot)
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
static constexpr int32_t ClassAbsent
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static constexpr int32_t ClassPresent
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
static constexpr int StringTableCap
static void SetStr(CUtlString &out, std::string_view text)
static constexpr int StringTableHeadroom
std::expected< void, Error > Status
Definition Result.hpp:67
constexpr int EveryoneSlot
Definition Slot.hpp:10
CUtlVector< CUtlString > StringTable
std::expected< T, Error > Result
Definition Result.hpp:64
Fn< void(void *, int32_t, bool)> CustomHudSetInputCapture
Definition Bindings.hpp:61
static Error Failed(std::string detail)
Definition Result.hpp:59
static Error Invalid(std::string detail)
Definition Result.hpp:51
static Error Engine(std::string detail)
Definition Result.hpp:58
static Error Unsupported(std::string detail)
Definition Result.hpp:57
static Error NotFound(std::string detail)
Definition Result.hpp:49