VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
DownloadQueue.cpp
Go to the documentation of this file.
2
4#include <algorithm>
5#include <string>
6#include <utility>
7
8namespace VoltMod
9{
10
12{
13 if (id == 0)
14 {
15 return false;
16 }
17
18 const auto found = std::ranges::find(_required, id, &Requirement::Id);
19 if (found != _required.end())
20 {
21 ++found->Holders;
22 }
23 else
24 {
25 _required.push_back({.Id = id, .Holders = 1});
26 }
27 return true;
28}
29
31{
32 const auto found = std::ranges::find(_required, id, &Requirement::Id);
33 if (found != _required.end() && --found->Holders <= 0)
34 {
35 _required.erase(found);
36 }
37}
38
39std::vector<uint64_t> DownloadQueue::Required() const
40{
41 std::vector<uint64_t> ids;
42 ids.reserve(_required.size());
43 for (const Requirement& requirement : _required)
44 {
45 ids.push_back(requirement.Id);
46 }
47 return ids;
48}
49
50const DownloadQueue::Client* DownloadQueue::FindClient(int64_t steamId) const
51{
52 const auto found = _clients.find(steamId);
53 return found != _clients.end() ? &found->second : nullptr;
54}
55
56std::vector<uint64_t> DownloadQueue::MissingFor(int64_t steamId) const
57{
58 const Client* client = FindClient(steamId);
59 std::vector<uint64_t> missing = Required();
60 if (client)
61 {
62 std::erase_if(missing, [client](uint64_t id) { return std::ranges::contains(client->Downloaded, id); });
63 }
64 return missing;
65}
66
68{
69 const Client* client = FindClient(steamId);
70 if (!client)
71 {
72 return {};
73 }
74
75 std::vector<uint64_t> ids = Required();
76 std::erase_if(
77 ids, [client](uint64_t id) { return id != client->Sending && !std::ranges::contains(client->Downloaded, id); });
78 return ids;
79}
80
82{
83 const Client* client = FindClient(steamId);
84 if (!client)
85 {
86 return !_required.empty();
87 }
88
89 const auto missing = [client](const Requirement& requirement) {
90 return !std::ranges::contains(client->Downloaded, requirement.Id);
91 };
92 return std::ranges::any_of(_required, missing);
93}
94
96{
97 const std::vector<uint64_t> missing = MissingFor(steamId);
98 if (missing.empty())
99 {
100 return {};
101 }
102
103 Client& client = _clients[steamId];
104 const uint64_t next = missing.front();
105
106 // The same addon coming round again means the last offer was not taken.
107 client.Attempts = (client.Sending == next) ? client.Attempts + 1 : 1;
108 if (client.Attempts > maxAttempts)
109 {
110 return {.Action = AddonAction::Kick, .Id = next};
111 }
112
113 client.Sending = next;
114 client.SentAt = now;
115 return {.Action = AddonAction::Send, .Id = next, .Remaining = missing.size() - 1};
116}
117
119 int maxAttempts)
120{
121 if (!reconnect)
122 {
124 }
125
126 // The client handles only the first addon; the rest wait for a later reconnect.
127 const std::vector<uint64_t> listed = ParseAddonList(addons);
128 if (listed.empty())
129 {
130 // A client unmounts whatever the map change message does not name.
131 const std::vector<uint64_t> downloaded = ClientMountList(steamId);
132 if (downloaded.empty())
133 {
134 return {};
135 }
136 return {.Action = AddonAction::KeepMounted, .Id = downloaded.front()};
137 }
138
139 MarkSending(steamId, listed.front(), now);
140 if (listed.size() == 1)
141 {
142 return {};
143 }
144 return {.Action = AddonAction::TrimToFirst, .Id = listed.front(), .Remaining = listed.size() - 1};
145}
146
148{
149 if (id == 0)
150 {
151 return;
152 }
153
154 Client& client = _clients[steamId];
155 client.Sending = id;
156 client.SentAt = now;
157 client.Attempts = 0;
158}
159
161{
162 const auto found = _clients.find(steamId);
163 if (found == _clients.end())
164 {
165 return;
166 }
167
168 Client& client = found->second;
169 client.LeftAt = 0.0;
170 if (client.Sending == 0)
171 {
172 return;
173 }
174 if (now - client.SentAt <= timeoutSec)
175 {
176 if (!std::ranges::contains(client.Downloaded, client.Sending))
177 {
178 client.Downloaded.push_back(client.Sending);
179 }
180 client.Attempts = 0;
181 }
182 client.Sending = 0;
183}
184
186{
187 if (const auto found = _clients.find(steamId); found != _clients.end())
188 {
189 found->second.LeftAt = now;
190 }
191 std::erase_if(_clients, [now, forgetAfterSec](const auto& entry) {
192 const double leftAt = entry.second.LeftAt;
193 return leftAt > 0.0 && now - leftAt > forgetAfterSec;
194 });
195}
196
198{
199 _clients.clear();
200}
201
202/** The comma-separated entries of @p field, verbatim; none for an empty field. */
203static std::vector<std::string_view> SplitAddonList(std::string_view field)
204{
205 std::vector<std::string_view> entries;
206
207 while (!field.empty())
208 {
209 const size_t comma = field.find(',');
210 entries.push_back(field.substr(0, comma));
211
212 if (comma == std::string_view::npos)
213 {
214 break;
215 }
216 field.remove_prefix(comma + 1);
217 if (field.empty())
218 {
219 entries.emplace_back();
220 }
221 }
222
223 return entries;
224}
225
226std::vector<uint64_t> ParseAddonList(std::string_view field)
227{
228 std::vector<uint64_t> ids;
229 for (std::string_view entry : SplitAddonList(field))
230 {
231 if (auto id = ParseUInt64(entry); id && *id != 0)
232 {
233 ids.push_back(*id);
234 }
235 }
236 return ids;
237}
238
239std::vector<uint64_t> AppendToAddonList(std::string& field, const std::vector<uint64_t>& ids)
240{
241 const std::vector<uint64_t> named = ParseAddonList(field);
242 std::vector<uint64_t> appended;
243
244 for (uint64_t id : ids)
245 {
246 if (id == 0 || std::ranges::contains(named, id) || std::ranges::contains(appended, id))
247 {
248 continue;
249 }
250
251 if (!field.empty())
252 {
253 field += ',';
254 }
255 field += std::to_string(id);
256 appended.push_back(id);
257 }
258
259 return appended;
260}
261
262void RemoveFromAddonList(std::string& field, const std::vector<uint64_t>& ids)
263{
264 std::vector<std::string_view> entries = SplitAddonList(field);
265 for (uint64_t id : ids)
266 {
267 const auto at =
268 std::ranges::find_if(entries, [id](std::string_view entry) { return ParseUInt64(entry) == id; });
269 if (at != entries.end())
270 {
271 entries.erase(at);
272 }
273 }
274
275 std::string kept;
276 kept.reserve(field.size());
277 for (size_t i = 0; i < entries.size(); ++i)
278 {
279 if (i > 0)
280 {
281 kept += ',';
282 }
283 kept += entries[i];
284 }
285 field = std::move(kept);
286}
287
288} // namespace VoltMod
bool Add(uint64_t id)
AddonDecision DecideJoinMessage(int64_t steamId, bool reconnect, std::string_view addons, double now, int maxAttempts)
bool HasMissing(int64_t steamId) const
void Remove(uint64_t id)
void ClientReconnected(int64_t steamId, double now, double timeoutSec)
std::vector< uint64_t > ClientMountList(int64_t steamId) const
void MarkSending(int64_t steamId, uint64_t id, double now)
std::vector< uint64_t > Required() const
void ClientLeft(int64_t steamId, double now, double forgetAfterSec)
AddonDecision NextToSend(int64_t steamId, double now, int maxAttempts)
std::vector< uint64_t > MissingFor(int64_t steamId) const
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::vector< uint64_t > AppendToAddonList(std::string &field, const std::vector< uint64_t > &ids)
std::optional< uint64_t > ParseUInt64(std::string_view text)
Definition Strings.cpp:24
void RemoveFromAddonList(std::string &field, const std::vector< uint64_t > &ids)
std::vector< uint64_t > ParseAddonList(std::string_view field)
@ TrimToFirst
the message names several addons; keep only AddonDecision::Id
@ Kick
the client refused AddonDecision::Id too often
@ Send
point the client at AddonDecision::Id and wait for its reconnect
@ KeepMounted
a map change names no addon; name AddonDecision::Id so the client keeps it
static std::vector< std::string_view > SplitAddonList(std::string_view field)