TrinityCore
Loading...
Searching...
No Matches
GameUtilitiesService.cpp
Go to the documentation of this file.
1/*
2 * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
3 *
4 * This program is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License as published by the
6 * Free Software Foundation; either version 2 of the License, or (at your
7 * option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 * more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
20#include "DatabaseEnv.h"
21#include "Hash.h"
22#include "Log.h"
23#include "MapUtils.h"
24#include "ProtobufJSON.h"
25#include "RealmList.h"
26#include "RealmList.pb.h"
27#include "Session.h"
28#include "Types.h"
29#include <zlib.h>
30
31namespace Battlenet::Services
32{
33namespace Shared
34{
35std::string_view GameUtilities::ParseParamName(std::string_view command)
36{
37 if (command.starts_with("Command_"sv))
38 {
39 size_t pos = command.rfind('_');
40 if (pos != std::string_view::npos)
41 command.remove_suffix(command.length() - pos);
42 }
43
44 return command;
45}
46
47Variant* GameUtilities::FindParamValue(std::vector<std::pair<std::string_view, Variant>>& params, std::string_view paramName)
48{
49 auto itr = std::ranges::find(params, paramName, Trinity::Containers::MapKey);
50 return itr != params.end() ? &itr->second : nullptr;
51}
52
53std::vector<uint8> GameUtilities::CompressJson(std::string const& json)
54{
55 uLong uncompressedLength = uLong(json.length() + 1);
56 uLong compressedLength = compressBound(uLong(json.length()));
57 std::vector<uint8> compressed(compressedLength + 4);
58 memcpy(compressed.data(), &uncompressedLength, sizeof(uncompressedLength));
59
60 if (compress(compressed.data() + 4, &compressedLength, reinterpret_cast<uint8 const*>(json.data()), uncompressedLength) != Z_OK)
61 {
62 compressed.clear();
63 return compressed;
64 }
65
66 compressed.resize(compressedLength + 4); // trim excess bytes
67 return compressed;
68}
69
71 std::vector<std::pair<std::string_view, Variant>>& params,
72 std::vector<std::pair<std::string_view, Variant>>& responseValues)
73{
74 auto command = std::ranges::find_if(params,
75 [](std::string_view paramName) { return paramName.starts_with("Command_"sv); },
77
78 if (command == params.end())
79 {
80 TC_LOG_ERROR("session.rpc", "{} sent ClientRequest with no command.", session->GetClientInfo());
82 }
83
84 switch (Trinity::HashFnv1a<>::GetHash(command->first))
85 {
86 case Trinity::HashFnv1a<>::GetHash("Command_LastCharPlayedRequest_v1"sv):
87 return GetLastCharPlayed(session, params, responseValues);
88 case Trinity::HashFnv1a<>::GetHash("Command_RealmListTicketRequest_v1"sv):
89 return GetRealmListTicket(session, params, responseValues);
90 case Trinity::HashFnv1a<>::GetHash("Command_RealmListRequest_v1"sv):
91 return GetRealmList(session, params, responseValues);
92 case Trinity::HashFnv1a<>::GetHash("Command_RealmJoinRequest_v1"sv):
93 return JoinRealm(session, params, responseValues);
94 case Trinity::HashFnv1a<>::GetHash("Command_FetchBleepProxiesRequest_v1"sv):
95 return GetBleepProxies(session, params, responseValues);
96 default:
97 break;
98 }
99
100 TC_LOG_ERROR("session.rpc", "{} sent ClientRequest with unknown command {}.", session->GetClientInfo(), command->first);
102}
103
104uint32 GameUtilities::HandleGetAllValuesForAttribute(Session const* session, std::string_view command, std::vector<Variant>& responseValues)
105{
106 if (!session->IsAuthed())
107 return ERROR_DENIED;
108
109 switch (Trinity::HashFnv1a<>::GetHash(command))
110 {
111 case Trinity::HashFnv1a<>::GetHash("Command_RealmListRequest_v1"sv):
112 for (std::string& subRegion : sRealmList->GetSubRegions())
113 responseValues.emplace_back(std::move(subRegion));
114 return ERROR_OK;
115 default:
116 break;
117 }
118
119 TC_LOG_ERROR("session.rpc", "{} sent GetAllValuesForAttributeRequest with unknown command {}.", session->GetClientInfo(), command);
121}
122
124 std::vector<std::pair<std::string_view, Variant>>& params,
125 std::vector<std::pair<std::string_view, Variant>>& responseValues)
126{
127 Variant const* subRegion = FindParamValue(params, "Command_LastCharPlayedRequest_v1");
128 if (!subRegion || !std::holds_alternative<std::string>(*subRegion))
130
131 if (LastPlayedCharacterInfo const* lastPlayerChar = session->GetLastPlayedCharacter(std::get<std::string>(*subRegion)))
132 {
133 std::string realmEntryJson = sRealmList->GetRealmEntryJSON(lastPlayerChar->RealmId, session->GetBuild(), session->GetGameAccountInfo()->SecurityLevel);
134 if (realmEntryJson.empty())
136
137 std::vector<uint8> guidData(sizeof(lastPlayerChar->CharacterGUID));
138 std::memcpy(guidData.data(), &lastPlayerChar->CharacterGUID, sizeof(lastPlayerChar->CharacterGUID));
139
140 ::JSON::RealmList::UtilityInfo utilityInfo;
141 utilityInfo.set_realmpermissions(0x200);
142
143 std::vector<uint8> utilityInfoJson = CompressJson("JSONUtilityInfo:" + ::JSON::Serialize(utilityInfo));
144
145 responseValues.emplace_back("Param_RealmEntry"sv, CompressJson("JamJSONRealmEntry:" + realmEntryJson));
146 responseValues.emplace_back("Param_CharacterName"sv, lastPlayerChar->CharacterName);
147 responseValues.emplace_back("Param_CharacterGUID"sv, std::move(guidData));
148 responseValues.emplace_back("Param_LastPlayedTime"sv, int64(lastPlayerChar->LastPlayedTime));
149 responseValues.emplace_back("Param_UtilityInfo"sv, std::move(utilityInfoJson));
150 }
151
152 return ERROR_OK;
153}
154
156 std::vector<std::pair<std::string_view, Variant>>& params,
157 std::vector<std::pair<std::string_view, Variant>>& responseValues)
158{
159 GameAccountInfo const* gameAccountInfo = nullptr;
160
161 if (Variant const* identity = FindParamValue(params, "Param_Identity"); identity && std::holds_alternative<std::vector<uint8>>(*identity))
162 {
163 std::string_view json = { reinterpret_cast<char const*>(std::get<std::vector<uint8>>(*identity).data()), std::get<std::vector<uint8>>(*identity).size() };
164 ::JSON::RealmList::RealmListTicketIdentity data;
165 std::size_t jsonStart = json.find(':');
166 if (jsonStart != std::string_view::npos && ::JSON::Deserialize(json.substr(jsonStart + 1), &data))
167 gameAccountInfo = session->GetGameAccountInfo(data.gameaccountid());
168 }
169
170 if (!gameAccountInfo)
172
173 if (gameAccountInfo->IsPermanenetlyBanned)
175 if (gameAccountInfo->IsBanned)
177
178 ClientBuild::VariantId clientBuildVariant = { };
179 Optional<std::array<uint8, 32>> clientSecret;
180 if (Variant const* clientInfo = FindParamValue(params, "Param_ClientInfo"))
181 {
182 std::string_view json = { reinterpret_cast<char const*>(std::get<std::vector<uint8>>(*clientInfo).data()), std::get<std::vector<uint8>>(*clientInfo).size() };
183 ::JSON::RealmList::RealmListTicketClientInformation data;
184 std::size_t jsonStart = json.find(':');
185 if (jsonStart != std::string_view::npos && ::JSON::Deserialize(json.substr(jsonStart + 1), &data))
186 {
187 if (data.info().secret().size() == 32)
188 memcpy(clientSecret.emplace().data(), data.info().secret().data(), data.info().secret().size());
189
190 clientBuildVariant =
191 {
192 .Platform = ClientBuild::Platform::Id(data.info().platformtype()),
193 .Arch = ClientBuild::Arch::Id(data.info().clientarch()),
194 .Type = ClientBuild::Type::Id(data.info().type())
195 };
196 }
197 }
198
199 if (!clientSecret)
201
202 session->SetClientInfo(gameAccountInfo->Id, clientBuildVariant, *clientSecret);
203
205 stmt->setString(0, session->GetRemoteIpAddress().to_string());
206 stmt->setUInt8(1, GetLocaleByName(session->GetLocale()));
207 stmt->setString(2, session->GetOS());
208 stmt->setUInt32(3, session->GetAccountId());
209
210 LoginDatabase.Execute(stmt);
211
212 std::vector<uint8> realmListTicket;
213 std::ranges::copy("AuthRealmListTicket\0"sv, std::back_inserter(realmListTicket));
214 responseValues.emplace_back("Param_RealmListTicket"sv, std::move(realmListTicket));
215
216 return ERROR_OK;
217}
218
220 std::vector<std::pair<std::string_view, Variant>>& params,
221 std::vector<std::pair<std::string_view, Variant>>& responseValues)
222{
223 std::string subRegionId;
224 if (Variant const* subRegion = FindParamValue(params, "Command_RealmListRequest_v1"); subRegion && std::holds_alternative<std::string>(*subRegion))
225 subRegionId = std::get<std::string>(*subRegion);
226
227 std::vector<uint8> realmListJson = sRealmList->GetRealmList(session->GetBuild(), session->GetGameAccountInfo()->SecurityLevel, subRegionId);
228
229 if (realmListJson.empty())
231
232 ::JSON::RealmList::RealmCharacterCountList realmCharacterCounts;
233 for (auto const& [realmAddress, count] : session->GetGameAccountInfo()->CharacterCounts)
234 {
235 ::JSON::RealmList::RealmCharacterCountEntry* countEntry = realmCharacterCounts.add_counts();
236 countEntry->set_wowrealmaddress(realmAddress);
237 countEntry->set_count(count);
238 }
239
240 std::vector<uint8> characterCountsJson = CompressJson("JSONRealmCharacterCountList:" + ::JSON::Serialize(realmCharacterCounts));
241
242 if (characterCountsJson.empty())
244
245 responseValues.emplace_back("Param_RealmList"sv, std::move(realmListJson));
246 responseValues.emplace_back("Param_CharacterCountList"sv, std::move(characterCountsJson));
247
248 return ERROR_OK;
249}
250
252 std::vector<std::pair<std::string_view, Variant>>& params,
253 std::vector<std::pair<std::string_view, Variant>>& responseValues)
254{
255 Variant const* realmAddress = FindParamValue(params, "Param_RealmAddress");
256 if (!realmAddress || !std::holds_alternative<uint64>(*realmAddress))
258
259 RealmJoinResult result = sRealmList->JoinRealm(std::get<uint64>(*realmAddress), session->GetBuild(), session->GetBuildVariant(),
260 session->GetRemoteIpAddress(), session->GetClientSecret(), GetLocaleByName(session->GetLocale()), session->GetOS(),
261 session->GetTimezoneOffset(), session->GetGameAccountInfo()->Name, session->GetGameAccountInfo()->SecurityLevel);
262
263 if (result.Result == ERROR_OK)
264 {
265 responseValues.emplace_back("Param_RealmJoinTicket"sv, std::move(result.JoinTicket));
266 responseValues.emplace_back("Param_ServerAddresses"sv, std::move(result.ServerAddresses));
267 responseValues.emplace_back("Param_JoinSecret"sv, std::move(result.JoinSecret));
268 }
269
270 return result.Result;
271}
272
274 std::vector<std::pair<std::string_view, Variant>>& /*params*/,
275 std::vector<std::pair<std::string_view, Variant>>& responseValues)
276{
277 std::vector<uint8> proxyListJson = CompressJson("JSONBleepProxyList:" + ::JSON::Serialize(::JSON::RealmList::BleepProxyList()));
278
279 if (proxyListJson.empty())
281
282 responseValues.emplace_back("Param_BleepProxyList"sv, std::move(proxyListJson));
283
284 return ERROR_OK;
285}
286}
287
288namespace V1
289{
290Battlenet::Services::Variant FromProto(bgs::protocol::Variant const& from)
291{
292 if (from.has_bool_value())
293 return from.bool_value();
294 if (from.has_int_value())
295 return from.int_value();
296 if (from.has_float_value())
297 return from.float_value();
298 if (from.has_string_value())
299 return from.string_value();
300 if (from.has_blob_value())
301 return Variant{ std::in_place_type<std::vector<uint8>>,
302 reinterpret_cast<uint8 const*>(from.blob_value().data()),
303 reinterpret_cast<uint8 const*>(from.blob_value().data()) + from.blob_value().size() };
304 if (from.has_uint_value())
305 return from.uint_value();
306
307 return { };
308}
309
310void ToProto(Battlenet::Services::Variant const& from, bgs::protocol::Variant* to)
311{
312 std::visit([to]<typename T>(T const& value)
313 {
314 if constexpr (std::is_same_v<T, bool>)
315 to->set_bool_value(value);
316 else if constexpr (std::is_same_v<T, int64>)
317 to->set_int_value(value);
318 else if constexpr (std::is_same_v<T, double>)
319 to->set_float_value(value);
320 else if constexpr (std::is_same_v<T, std::string>)
321 to->set_string_value(value);
322 else if constexpr (std::is_same_v<T, std::vector<uint8>>)
323 to->set_blob_value(value.data(), value.size());
324 else if constexpr (std::is_same_v<T, uint64>)
325 to->set_uint_value(value);
326 else
327 static_assert(Trinity::dependant_false_v<T>);
328 }, from);
329}
330
334
335uint32 GameUtilities::HandleProcessClientRequest(game_utilities::v1::ClientRequest const* request, game_utilities::v1::ClientResponse* response, std::function<void(ServiceBase*, uint32, ::google::protobuf::Message const*)>& /*continuation*/)
336{
337 if (!_session->IsAuthed())
338 return ERROR_DENIED;
339
340 std::vector<std::pair<std::string_view, Variant>> params;
341 std::vector<std::pair<std::string_view, Variant>> responseValues;
342
343 for (Attribute const& attribute : request->attribute())
344 {
345 if (!attribute.has_name() || !attribute.has_value())
346 continue;
347
348 params.emplace_back(Shared::GameUtilities::ParseParamName(attribute.name()), FromProto(attribute.value()));
349 }
350
352
353 for (auto&& [name, value] : responseValues)
354 {
355 Attribute* attr = response->add_attribute();
356 attr->set_name(name.data(), name.length());
357 ToProto(value, attr->mutable_value());
358 }
359
360 return result;
361}
362
363uint32 GameUtilities::HandleGetAllValuesForAttribute(game_utilities::v1::GetAllValuesForAttributeRequest const* request, game_utilities::v1::GetAllValuesForAttributeResponse* response, std::function<void(ServiceBase*, uint32, ::google::protobuf::Message const*)>& /*continuation*/)
364{
365 if (!_session->IsAuthed())
366 return ERROR_DENIED;
367
368 std::vector<Variant> responseValues;
369
371
372 for (Variant& value : responseValues)
373 ToProto(value, response->add_attribute_value());
374
375 return result;
376}
377}
378
379namespace V2
380{
381Battlenet::Services::Variant FromProto(bgs::protocol::v2::Variant const& from)
382{
383 switch (from.type_case())
384 {
385 case v2::Variant::kBoolValue:
386 return from.bool_value();
387 case v2::Variant::kIntValue:
388 return from.int_value();
389 case v2::Variant::kFloatValue:
390 return from.float_value();
391 case v2::Variant::kStringValue:
392 return from.string_value();
393 case v2::Variant::kBlobValue:
394 return Variant{ std::in_place_type<std::vector<uint8>>,
395 reinterpret_cast<uint8 const*>(from.blob_value().data()),
396 reinterpret_cast<uint8 const*>(from.blob_value().data()) + from.blob_value().size() };
397 case v2::Variant::kUintValue:
398 return from.uint_value();
399 default:
400 break;
401 }
402
403 return { };
404}
405
406void ToProto(Battlenet::Services::Variant const& from, bgs::protocol::v2::Variant* to)
407{
408 std::visit([to]<typename T>(T const& value)
409 {
410 if constexpr (std::is_same_v<T, bool>)
411 to->set_bool_value(value);
412 else if constexpr (std::is_same_v<T, int64>)
413 to->set_int_value(value);
414 else if constexpr (std::is_same_v<T, double>)
415 to->set_float_value(value);
416 else if constexpr (std::is_same_v<T, std::string>)
417 to->set_string_value(value);
418 else if constexpr (std::is_same_v<T, std::vector<uint8>>)
419 to->set_blob_value(value.data(), value.size());
420 else if constexpr (std::is_same_v<T, uint64>)
421 to->set_uint_value(value);
422 else
423 static_assert(Trinity::dependant_false_v<T>);
424 }, from);
425}
426
430
431uint32 GameUtilities::HandleProcessTask(game_utilities::v2::client::ProcessTaskRequest const* request, game_utilities::v2::client::ProcessTaskResponse* response, std::function<void(ServiceBase*, uint32, ::google::protobuf::Message const*)>& /*continuation*/)
432{
433 if (!_session->IsAuthed())
434 return ERROR_DENIED;
435
436 std::vector<std::pair<std::string_view, Variant>> params;
437 std::vector<std::pair<std::string_view, Variant>> responseValues;
438
439 for (v2::Attribute const& attribute : request->attribute())
440 {
441 if (!attribute.has_name() || !attribute.has_value())
442 continue;
443
444 params.emplace_back(Shared::GameUtilities::ParseParamName(attribute.name()), FromProto(attribute.value()));
445 }
446
448
449 for (auto&& [name, value] : responseValues)
450 {
451 v2::Attribute* attr = response->add_result();
452 attr->set_name(name.data(), name.length());
453 ToProto(value, attr->mutable_value());
454 }
455
456 return result;
457}
458
459uint32 GameUtilities::HandleGetAllValuesForAttribute(game_utilities::v2::client::GetAllValuesForAttributeRequest const* request, game_utilities::v2::client::GetAllValuesForAttributeResponse* response, std::function<void(ServiceBase*, uint32, google::protobuf::Message const*)>& /*continuation*/)
460{
461 if (!_session->IsAuthed())
462 return ERROR_DENIED;
463
464 std::vector<Variant> responseValues;
465
467
468 for (Variant& value : responseValues)
469 ToProto(value, response->add_attribute_value());
470
471 return result;
472}
473}
474}
@ ERROR_GAME_ACCOUNT_BANNED
@ ERROR_RPC_NOT_IMPLEMENTED
@ ERROR_RPC_MALFORMED_REQUEST
@ ERROR_UTIL_SERVER_INVALID_IDENTITY_ARGS
@ ERROR_UTIL_SERVER_FAILED_TO_SERIALIZE_RESPONSE
@ ERROR_GAME_ACCOUNT_SUSPENDED
@ ERROR_WOW_SERVICES_DENIED_REALM_LIST_TICKET
@ ERROR_UTIL_SERVER_UNKNOWN_REALM
LocaleConstant GetLocaleByName(std::string_view name)
Definition Common.cpp:36
DatabaseWorkerPool< LoginDatabaseConnection > LoginDatabase
Accessor to the realm/login database.
uint8_t uint8
Definition Define.h:156
int64_t int64
Definition Define.h:149
uint32_t uint32
Definition Define.h:154
std::unordered_set< uint32 > params[2]
#define TC_LOG_ERROR(filterType__, message__,...)
Definition Log.h:182
@ LOGIN_UPD_BNET_LAST_LOGIN_INFO
std::optional< T > Optional
Optional helper class to wrap optional values within.
Definition Optional.h:25
#define sRealmList
Definition RealmList.h:95
static uint32 GetLastCharPlayed(Session const *session, std::vector< std::pair< std::string_view, Variant > > &params, std::vector< std::pair< std::string_view, Variant > > &responseValues)
static Variant * FindParamValue(std::vector< std::pair< std::string_view, Variant > > &params, std::string_view paramName)
static uint32 HandleClientRequest(Session *session, std::vector< std::pair< std::string_view, Variant > > &params, std::vector< std::pair< std::string_view, Variant > > &responseValues)
static std::string_view ParseParamName(std::string_view command)
static uint32 JoinRealm(Session const *session, std::vector< std::pair< std::string_view, Variant > > &params, std::vector< std::pair< std::string_view, Variant > > &responseValues)
static uint32 GetRealmList(Session const *session, std::vector< std::pair< std::string_view, Variant > > &params, std::vector< std::pair< std::string_view, Variant > > &responseValues)
static uint32 GetBleepProxies(Session const *session, std::vector< std::pair< std::string_view, Variant > > &params, std::vector< std::pair< std::string_view, Variant > > &responseValues)
static uint32 HandleGetAllValuesForAttribute(Session const *session, std::string_view command, std::vector< Variant > &responseValues)
static uint32 GetRealmListTicket(Session *session, std::vector< std::pair< std::string_view, Variant > > &params, std::vector< std::pair< std::string_view, Variant > > &responseValues)
static std::vector< uint8 > CompressJson(std::string const &json)
uint32 HandleProcessClientRequest(game_utilities::v1::ClientRequest const *request, game_utilities::v1::ClientResponse *response, std::function< void(ServiceBase *, uint32, ::google::protobuf::Message const *)> &continuation) override
uint32 HandleGetAllValuesForAttribute(game_utilities::v1::GetAllValuesForAttributeRequest const *request, game_utilities::v1::GetAllValuesForAttributeResponse *response, std::function< void(ServiceBase *, uint32, ::google::protobuf::Message const *)> &continuation) override
uint32 HandleProcessTask(game_utilities::v2::client::ProcessTaskRequest const *request, game_utilities::v2::client::ProcessTaskResponse *response, std::function< void(ServiceBase *, uint32, ::google::protobuf::Message const *)> &continuation) override
uint32 HandleGetAllValuesForAttribute(game_utilities::v2::client::GetAllValuesForAttributeRequest const *request, game_utilities::v2::client::GetAllValuesForAttributeResponse *response, std::function< void(ServiceBase *, uint32, ::google::protobuf::Message const *)> &continuation) override
std::string GetClientInfo() const
Definition Session.cpp:345
Minutes GetTimezoneOffset() const
Definition Session.h:112
uint32 GetAccountId() const
Definition Session.h:100
std::array< uint8, 32 > GetClientSecret() const
Definition Session.h:113
uint32 GetBuild() const
Definition Session.h:110
LastPlayedCharacterInfo const * GetLastPlayedCharacter(std::string_view subRegion) const
Definition Session.cpp:237
void SetClientInfo(uint32 gameAccountId, ClientBuild::VariantId buildVariant, std::array< uint8, 32 > const &clientSecret)
Definition Session.cpp:230
bool IsAuthed() const
Definition Session.h:94
ClientBuild::VariantId GetBuildVariant() const
Definition Session.h:111
std::string const & GetLocale() const
Definition Session.h:108
std::string const & GetOS() const
Definition Session.h:109
boost::asio::ip::address const & GetRemoteIpAddress() const
Definition Session.h:89
GameAccountInfo const * GetGameAccountInfo() const
Definition Session.h:104
void setString(uint8 index, std::string &&value)
void setUInt32(uint8 index, uint32 value)
void setUInt8(uint8 index, uint8 value)
Battlenet::Services::Variant FromProto(bgs::protocol::Variant const &from)
void ToProto(Battlenet::Services::Variant const &from, bgs::protocol::Variant *to)
Battlenet::Services::Variant FromProto(bgs::protocol::v2::Variant const &from)
void ToProto(Battlenet::Services::Variant const &from, bgs::protocol::v2::Variant *to)
std::variant< bool, int64, double, std::string, std::vector< uint8 >, uint64 > Variant
TC_SHARED_API std::string Serialize(google::protobuf::Message const &message)
TC_SHARED_API bool Deserialize(std::string_view json, google::protobuf::Message *message)
constexpr auto MapKey
Definition MapUtils.h:72
AccountTypes SecurityLevel
Definition Session.h:59
std::unordered_map< uint32, uint8 > CharacterCounts
Definition Session.h:61
FourCC< Arch > Id
FourCC< Platform > Id
FourCC< Type > Id
ClientBuild::Platform::Id Platform
std::vector< uint8 > JoinSecret
Definition RealmList.h:43
std::vector< uint8 > JoinTicket
Definition RealmList.h:41
std::vector< uint8 > ServerAddresses
Definition RealmList.h:42
static constexpr T GetHash(std::span< V, Extent > data) noexcept
Definition Hash.h:132