diff --git a/csgo_gc/gc_client.cpp b/csgo_gc/gc_client.cpp index 255d735..401bc02 100644 --- a/csgo_gc/gc_client.cpp +++ b/csgo_gc/gc_client.cpp @@ -2,6 +2,7 @@ #include "gc_client.h" #include "graffiti.h" #include "keyvalue.h" +#include "networking_shared.h" ClientGC::ClientGC(uint64_t steamId) : m_steamId{ steamId } @@ -10,6 +11,13 @@ ClientGC::ClientGC(uint64_t steamId) // also called from ServerGC's constructor Graffiti::Initialize(); + // The inventory exists before the worker thread starts, so seed the + // round_mvp event cache here and keep later refreshes on the worker thread. + if (m_inventory.EquippedMusicKitItemId(true)) + { + m_cachedMusicKitMVPs.store(static_cast(m_inventory.EquippedMusicKitMVPCount(false))); + } + StartThread(); Platform::Print("ClientGC spawned for user %llu\n", steamId); @@ -21,6 +29,69 @@ ClientGC::~ClientGC() Platform::Print("ClientGC destroyed\n"); } +uint32_t ClientGC::LocalPlayerMusicKitMVPsForRoundMVPEvent() const +{ + // round_mvp is observed before the worker-thread inventory mirror applies + // the local increment, so expose the post-MVP value here. + int32_t cachedMVPs = m_cachedMusicKitMVPs.load(); + return cachedMVPs >= 0 ? static_cast(cachedMVPs + 1) : 0; +} + +void ClientGC::RefreshCachedMusicKitMVPs() +{ + if (!m_inventory.EquippedMusicKitItemId(true)) + { + m_cachedMusicKitMVPs.store(-1); + SendMusicKitMVPStateToGameServer(); + return; + } + + m_cachedMusicKitMVPs.store(static_cast(m_inventory.EquippedMusicKitMVPCount(false))); + SendMusicKitMVPStateToGameServer(); +} + +void ClientGC::SendMusicKitMVPStateToGameServer() +{ + int32_t userId = m_localUserId.load(); + if (userId <= 0) + { + return; + } + + GCMessageWrite messageWrite{ k_EMsgNetworkMusicKitMVPState }; + + int32_t cachedMVPs = m_cachedMusicKitMVPs.load(); + uint32_t currentMVPs = cachedMVPs >= 0 ? static_cast(cachedMVPs) : 0; + uint32_t hasEquippedStatTrakMusicKit = cachedMVPs >= 0 ? 1u : 0u; + + messageWrite.WriteUint32(static_cast(userId)); + messageWrite.WriteUint32(hasEquippedStatTrakMusicKit); + messageWrite.WriteUint32(currentMVPs); + + Platform::Print("ClientGC: syncing music kit MVP state to server: userid=%d haskit=%u mvps=%u\n", + userId, + hasEquippedStatTrakMusicKit, + currentMVPs); + PostToHost(HostEvent::NetMessage, 0, messageWrite.Data(), messageWrite.Size()); +} + +void ClientGC::SyncLocalPlayerMusicKitState(int userId) +{ + if (userId <= 0) + { + return; + } + + int32_t previousUserId = m_localUserId.exchange(userId); + if (previousUserId != userId) + { + Platform::Print("ClientGC: local userid changed from %d to %d, syncing music kit state\n", + previousUserId, + userId); + SendMusicKitMVPStateToGameServer(); + } +} + void ClientGC::HandleEvent(GCEvent type, uint64_t id, const std::vector &buffer) { switch (type) @@ -37,6 +108,23 @@ void ClientGC::HandleEvent(GCEvent type, uint64_t id, const std::vector HandleSOCacheRequest(); break; + case GCEvent::LocalPlayerRoundMVP: + LocalPlayerRoundMVP(); + break; + + case GCEvent::SyncLocalPlayerMusicKitState: + if (buffer.size() == sizeof(uint32_t)) + { + uint32_t userId; + memcpy(&userId, buffer.data(), sizeof(userId)); + SyncLocalPlayerMusicKitState(static_cast(userId)); + } + else + { + assert(false); + } + break; + default: assert(false); break; @@ -315,6 +403,8 @@ void ClientGC::AdjustItemEquippedState(GCMessageRead &messageRead) return; } + RefreshCachedMusicKitMVPs(); + // let the gameserver know, too SendMessageToGame(true, k_ESOMsg_UpdateMultiple, update); } @@ -431,11 +521,12 @@ void ClientGC::IncrementKillCountAttribute(GCMessageRead &messageRead) return; } - assert(message.event_type() == 0); + assert(message.event_type() == 0 || message.event_type() == 1); CMsgSOSingleObject update; if (m_inventory.IncrementKillCountAttribute(message.item_id(), message.amount(), update)) { + RefreshCachedMusicKitMVPs(); SendMessageToGame(true, k_ESOMsg_Update, update); } else @@ -444,6 +535,29 @@ void ClientGC::IncrementKillCountAttribute(GCMessageRead &messageRead) } } +void ClientGC::LocalPlayerRoundMVP() +{ + // Music kit StatTrak progress is not driven by the retired official GC path, + // so mirror the increment locally when the client observes a local round_mvp. + uint64_t itemId = m_inventory.EquippedMusicKitItemId(true); + if (!itemId) + { + Platform::Print("LocalPlayerRoundMVP: local MVP without equipped StatTrak music kit\n"); + return; + } + + CMsgSOSingleObject update; + if (!m_inventory.IncrementKillCountAttribute(itemId, 1, update)) + { + Platform::Print("LocalPlayerRoundMVP: failed to increment music kit %llu\n", itemId); + return; + } + + RefreshCachedMusicKitMVPs(); + Platform::Print("LocalPlayerRoundMVP: incremented music kit %llu\n", itemId); + SendMessageToGame(true, k_ESOMsg_Update, update); +} + void ClientGC::ApplySticker(GCMessageRead &messageRead) { CMsgApplySticker message; diff --git a/csgo_gc/gc_client.h b/csgo_gc/gc_client.h index 1e52c7b..e92b963 100644 --- a/csgo_gc/gc_client.h +++ b/csgo_gc/gc_client.h @@ -9,6 +9,7 @@ class ClientGC final : public SharedGC public: ClientGC(uint64_t steamId); ~ClientGC(); + uint32_t LocalPlayerMusicKitMVPsForRoundMVPEvent() const; private: void HandleEvent(GCEvent type, uint64_t id, const std::vector &buffer) override; @@ -17,6 +18,9 @@ class ClientGC final : public SharedGC void HandleMessage(uint32_t type, const void *data, uint32_t size); void HandleNetMessage(const void *data, uint32_t size); void HandleSOCacheRequest(); + void RefreshCachedMusicKitMVPs(); + void SyncLocalPlayerMusicKitState(int userId); + void SendMusicKitMVPStateToGameServer(); // send to the local game and the game server we're connected to (if we're connected) void SendMessageToGame(bool sendToGameServer, uint32_t type, @@ -29,6 +33,8 @@ class ClientGC final : public SharedGC void ClientRequestJoinServerData(GCMessageRead &messageRead); void SetItemPositions(GCMessageRead &messageRead); void IncrementKillCountAttribute(GCMessageRead &messageRead); + // Increment the equipped StatTrak music kit when the local player receives round MVP. + void LocalPlayerRoundMVP(); void ApplySticker(GCMessageRead &messageRead); void StoreGetUserData(GCMessageRead &messageRead); void StorePurchaseInit(GCMessageRead &messageRead); @@ -50,6 +56,8 @@ class ClientGC final : public SharedGC const uint64_t m_steamId; Inventory m_inventory; + std::atomic m_localUserId{}; + std::atomic m_cachedMusicKitMVPs{ -1 }; // microtransactions, we only have one going at a time uint64_t m_transactionId{}; diff --git a/csgo_gc/gc_server.cpp b/csgo_gc/gc_server.cpp index 3db05a3..db638fd 100644 --- a/csgo_gc/gc_server.cpp +++ b/csgo_gc/gc_server.cpp @@ -3,6 +3,7 @@ #include "gc_const.h" #include "gc_const_csgo.h" #include "graffiti.h" +#include "networking_shared.h" // yuck!! needed for CSteamID (construct full id from account id) #include "steam/steamclientpublic.h" @@ -23,6 +24,20 @@ ServerGC::~ServerGC() Platform::Print("ServerGC destroyed\n"); } +bool ServerGC::RoundMVPMusicKitCountForUserId(int userId, int &musickitmvps) const +{ + std::lock_guard lock{ m_musicKitMVPStateMutex }; + + auto it = m_musicKitMVPStateByUserId.find(userId); + if (it == m_musicKitMVPStateByUserId.end() || !it->second.hasEquippedStatTrakMusicKit) + { + return false; + } + + musickitmvps = static_cast(it->second.currentMVPs + 1); + return true; +} + void ServerGC::HandleEvent(GCEvent type, uint64_t id, const std::vector &buffer) { switch (type) @@ -82,6 +97,16 @@ void ServerGC::HandleClientSOCacheUnsubscribe(uint64_t steamId) { Platform::Print("HandleClientSOCacheUnsubscribe: %llu\n", steamId); + { + std::lock_guard lock{ m_musicKitMVPStateMutex }; + auto stateIt = m_musicKitMVPStateBySteamId.find(steamId); + if (stateIt != m_musicKitMVPStateBySteamId.end()) + { + m_musicKitMVPStateByUserId.erase(stateIt->second.userId); + m_musicKitMVPStateBySteamId.erase(stateIt); + } + } + CMsgSOCacheUnsubscribed message; message.mutable_owner_soid()->set_type(SoIdTypeSteamId); message.mutable_owner_soid()->set_id(steamId); @@ -195,7 +220,12 @@ void ServerGC::HandleNetMessage(uint64_t steamId, const void *data, uint32_t siz if (!validate.IsProtobuf()) { - // all the allowed messages are protobuf based + if (validate.TypeUnmasked() == k_EMsgNetworkMusicKitMVPState) + { + UpdateMusicKitMVPState(steamId, validate); + return; + } + Platform::Print("ServerGC: ignoring non protobuf message %u from %llu\n", validate.TypeUnmasked(), steamId); return; @@ -252,6 +282,42 @@ void ServerGC::HandleNetMessage(uint64_t steamId, const void *data, uint32_t siz } } +void ServerGC::UpdateMusicKitMVPState(uint64_t steamId, GCMessageRead &messageRead) +{ + uint32_t userId = messageRead.ReadUint32(); + uint32_t hasEquippedStatTrakMusicKit = messageRead.ReadUint32(); + uint32_t currentMVPs = messageRead.ReadUint32(); + if (!messageRead.IsValid() || !userId || hasEquippedStatTrakMusicKit > 1) + { + Platform::Print("ServerGC: ignoring malformed music kit MVP state from %llu\n", steamId); + return; + } + + MusicKitMVPState state; + state.userId = static_cast(userId); + state.currentMVPs = currentMVPs; + state.hasEquippedStatTrakMusicKit = hasEquippedStatTrakMusicKit != 0; + + { + std::lock_guard lock{ m_musicKitMVPStateMutex }; + + auto &slot = m_musicKitMVPStateBySteamId[steamId]; + if (slot.userId > 0 && slot.userId != state.userId) + { + m_musicKitMVPStateByUserId.erase(slot.userId); + } + + slot = state; + m_musicKitMVPStateByUserId[state.userId] = state; + } + + Platform::Print("ServerGC: updated music kit MVP state from %llu: userid=%u haskit=%u mvps=%u\n", + steamId, + userId, + hasEquippedStatTrakMusicKit, + currentMVPs); +} + void ServerGC::SendServerWelcome() { // we don't care about anything in this message, just reply diff --git a/csgo_gc/gc_server.h b/csgo_gc/gc_server.h index cb01d21..d239b68 100644 --- a/csgo_gc/gc_server.h +++ b/csgo_gc/gc_server.h @@ -7,6 +7,7 @@ class ServerGC final : public SharedGC public: ServerGC(); ~ServerGC(); + bool RoundMVPMusicKitCountForUserId(int userId, int &musickitmvps) const; private: void HandleEvent(GCEvent type, uint64_t id, const std::vector &buffer) override; @@ -18,6 +19,18 @@ class ServerGC final : public SharedGC void SendServerWelcome(); void IncrementKillCountAttribute(GCMessageRead &messageRead); + void UpdateMusicKitMVPState(uint64_t steamId, GCMessageRead &messageRead); bool m_sentWelcome{}; + + struct MusicKitMVPState + { + int userId{}; + uint32_t currentMVPs{}; + bool hasEquippedStatTrakMusicKit{}; + }; + + mutable std::mutex m_musicKitMVPStateMutex; + std::unordered_map m_musicKitMVPStateBySteamId; + std::unordered_map m_musicKitMVPStateByUserId; }; diff --git a/csgo_gc/gc_shared.h b/csgo_gc/gc_shared.h index 0ba107f..c9fc210 100644 --- a/csgo_gc/gc_shared.h +++ b/csgo_gc/gc_shared.h @@ -14,6 +14,8 @@ enum class GCEvent Message, // id contains the message type, buffer contains the payload NetMessage, // id contains the recipient steam id, buffer contains the payload SOCacheRequest, // sent to client gc when connected to a gameserver + LocalPlayerRoundMVP, // sent to client gc when the local player earns round MVP + SyncLocalPlayerMusicKitState, // sent to client gc from the main thread, buffer contains the local userid ClientSOCacheUnsubscribe, // sent to server gc when a client disconnects, id contains the steam id }; diff --git a/csgo_gc/inventory.cpp b/csgo_gc/inventory.cpp index 60c3a51..732a267 100644 --- a/csgo_gc/inventory.cpp +++ b/csgo_gc/inventory.cpp @@ -986,6 +986,92 @@ bool Inventory::IncrementKillCountAttribute(uint64_t itemId, uint32_t amount, CM return false; } +static bool IsEquippedMusicKitItem(const CSOEconItem &item) +{ + // Music kits live in the shared no-team loadout. + for (const CSOEconItemEquipped &equipped : item.equipped_state()) + { + if (equipped.new_class() == 0 + && equipped.new_slot() == ItemSchema::LoadoutSlotMusicKit) + { + return true; + } + } + + return false; +} + +uint64_t Inventory::EquippedMusicKitItemId(bool statTrakOnly) const +{ + for (const auto &pair : m_items) + { + const CSOEconItem &item = pair.second; + if (!IsEquippedMusicKitItem(item)) + { + continue; + } + + bool hasKillEater = false; + bool isMusicKitScoreType = false; + + for (const CSOEconItemAttribute &attribute : item.attribute()) + { + if (attribute.def_index() == ItemSchema::AttributeKillEater) + { + hasKillEater = true; + } + else if (attribute.def_index() == ItemSchema::AttributeKillEaterScoreType + && m_itemSchema.AttributeUint32(&attribute) == 1) + { + isMusicKitScoreType = true; + } + } + + if (!hasKillEater) + { + if (statTrakOnly) + { + continue; + } + + return item.id(); + } + + if (isMusicKitScoreType) + { + return item.id(); + } + } + + return 0; +} + +uint32_t Inventory::EquippedMusicKitMVPCount(bool incrementForLocalMVP) const +{ + uint64_t itemId = EquippedMusicKitItemId(true); + if (!itemId) + { + return 0; + } + + auto it = m_items.find(itemId); + if (it == m_items.end()) + { + return 0; + } + + for (const CSOEconItemAttribute &attribute : it->second.attribute()) + { + if (attribute.def_index() == ItemSchema::AttributeKillEater) + { + uint32_t count = m_itemSchema.AttributeUint32(&attribute); + return incrementForLocalMVP ? count + 1 : count; + } + } + + return 0; +} + bool Inventory::NameItem(uint64_t nameTagId, uint64_t itemId, std::string_view name, diff --git a/csgo_gc/inventory.h b/csgo_gc/inventory.h index cad21a0..306e79e 100644 --- a/csgo_gc/inventory.h +++ b/csgo_gc/inventory.h @@ -47,6 +47,8 @@ class Inventory CMsgSOSingleObject &destroy, CMsgGCItemCustomizationNotification ¬ification); + uint64_t EquippedMusicKitItemId(bool statTrakOnly) const; + uint32_t EquippedMusicKitMVPCount(bool incrementForLocalMVP) const; bool IncrementKillCountAttribute(uint64_t itemId, uint32_t amount, CMsgSOSingleObject &update); bool NameItem(uint64_t nameTagId, diff --git a/csgo_gc/item_schema.h b/csgo_gc/item_schema.h index 6c573b1..fada894 100644 --- a/csgo_gc/item_schema.h +++ b/csgo_gc/item_schema.h @@ -172,6 +172,7 @@ class ItemSchema enum LoadoutSlot { + LoadoutSlotMusicKit = 54, LoadoutSlotGraffiti = 56 }; diff --git a/csgo_gc/networking_shared.h b/csgo_gc/networking_shared.h index c7889c4..27d42ef 100644 --- a/csgo_gc/networking_shared.h +++ b/csgo_gc/networking_shared.h @@ -11,4 +11,6 @@ enum ENetworkMsg : uint32_t { // sent by the server to client when they connect, data is the auth ticket k_EMsgNetworkConnect = (1u << 31) - 1, + // sent by the client to server to keep round_mvp musickitmvps injectable + k_EMsgNetworkMusicKitMVPState = (1u << 31) - 2, }; diff --git a/csgo_gc/platform.h b/csgo_gc/platform.h index 989f3da..f10583f 100644 --- a/csgo_gc/platform.h +++ b/csgo_gc/platform.h @@ -21,6 +21,11 @@ bool SteamClientPath(void *buffer, size_t bufferSize); // and get a pointer to the factory function (exported symbol CreateInterface) void *SteamClientFactory(const void *pathBuffer); +// get a module's factory function from an already loaded game module +// the module name is given in a platform-agnostic format +// (e.g. "engine" maps to engine.dll / engine_client.so / engine.dylib) +void *ModuleFactory(std::string_view moduleName); + // set an envar to the specified value even if it's already set void SetEnvVar(const char *name, const char *value); diff --git a/csgo_gc/platform_unix.cpp b/csgo_gc/platform_unix.cpp index aa3d3a3..4f5486d 100644 --- a/csgo_gc/platform_unix.cpp +++ b/csgo_gc/platform_unix.cpp @@ -239,6 +239,43 @@ void *SteamClientFactory(const void *pathBuffer) return dlsym(steamclient, "CreateInterface"); } +void *ModuleFactory(std::string_view moduleName) +{ + ModuleInfo moduleInfo; + +#if defined(__APPLE__) + std::string primaryName; + primaryName.assign(moduleName); + primaryName.append(".dylib"); + if (!GetModuleInfo(primaryName, moduleInfo)) + { + return nullptr; + } +#else + std::string primaryName; + primaryName.assign(moduleName); + primaryName.append("_client.so"); + + if (!GetModuleInfo(primaryName, moduleInfo)) + { + primaryName.assign(moduleName); + primaryName.append(".so"); + if (!GetModuleInfo(primaryName, moduleInfo)) + { + return nullptr; + } + } +#endif + + void *module = dlopen(moduleInfo.fullPath.c_str(), RTLD_NOW); + if (!module) + { + return nullptr; + } + + return dlsym(module, "CreateInterface"); +} + void SetEnvVar(const char *name, const char *value) { setenv(name, value, 1); diff --git a/csgo_gc/platform_windows.cpp b/csgo_gc/platform_windows.cpp index d9a4c90..d907da2 100644 --- a/csgo_gc/platform_windows.cpp +++ b/csgo_gc/platform_windows.cpp @@ -87,6 +87,21 @@ void *SteamClientFactory(const void *pathBuffer) return GetProcAddress(steamclient, "CreateInterface"); } +void *ModuleFactory(std::string_view moduleName) +{ + std::string actualModuleName; + actualModuleName.assign(moduleName); + actualModuleName.append(".dll"); + + HMODULE module = GetModuleHandleA(actualModuleName.c_str()); + if (!module) + { + return nullptr; + } + + return GetProcAddress(module, "CreateInterface"); +} + void SetEnvVar(const char *name, const char *value) { SetEnvironmentVariableA(name, value); diff --git a/csgo_gc/steam_hook.cpp b/csgo_gc/steam_hook.cpp index ed1b0b5..97b9ce5 100644 --- a/csgo_gc/steam_hook.cpp +++ b/csgo_gc/steam_hook.cpp @@ -8,6 +8,13 @@ #include "platform.h" #include +#ifdef _WIN32 +#include +#ifdef SendMessage +#undef SendMessage +#endif +#endif + struct SteamNetworkingIdentity; // defines STEAM_PRIVATE_API @@ -130,6 +137,306 @@ class GCWrapper final static GCWrapper *s_clientGC; static GCWrapper *s_serverGC; +struct PlayerInfo +{ + uint64_t version; + uint64_t xuid; + char name[128]; + int userId; + char guid[33]; + uint32_t friendsId; + char friendsName[128]; + bool fakePlayer; + bool isHltv; + uint32_t customFiles[4]; + unsigned char filesDownloaded; +}; + +class IGameEvent +{ +public: + virtual ~IGameEvent() = default; + virtual const char *GetName() const = 0; + virtual bool IsReliable() const = 0; + virtual bool IsLocal() const = 0; + virtual bool IsEmpty(const char *keyName = nullptr) const = 0; + virtual bool GetBool(const char *keyName = nullptr, bool defaultValue = false) const = 0; + virtual int GetInt(const char *keyName = nullptr, int defaultValue = 0) const = 0; + virtual uint64_t GetUint64(const char *keyName = nullptr, uint64_t defaultValue = 0) const = 0; + virtual float GetFloat(const char *keyName = nullptr, float defaultValue = 0.0f) const = 0; + virtual const char *GetString(const char *keyName = nullptr, const char *defaultValue = "") const = 0; + virtual const wchar_t *GetWString(const char *keyName = nullptr, const wchar_t *defaultValue = L"") const = 0; + virtual const void *GetPtr(const char *keyName = nullptr) const = 0; + virtual void SetBool(const char *keyName, bool value) = 0; + virtual void SetInt(const char *keyName, int value) = 0; +}; + +class IGameEventListener2 +{ +public: + virtual ~IGameEventListener2() = default; + virtual void FireGameEvent(IGameEvent *event) = 0; + virtual int GetEventDebugID() = 0; +}; + +class IGameEventManager2 +{ +public: + virtual ~IGameEventManager2() = default; + virtual int LoadEventsFromFile(const char *filename) = 0; + virtual void Reset() = 0; + virtual bool AddListener(IGameEventListener2 *listener, const char *name, bool serverSide) = 0; + virtual bool FindListener(IGameEventListener2 *listener, const char *name) = 0; + virtual void RemoveListener(IGameEventListener2 *listener) = 0; +}; + +class IVEngineClient +{ +public: + virtual void Unused0() = 0; + virtual void Unused1() = 0; + virtual void Unused2() = 0; + virtual void Unused3() = 0; + virtual void Unused4() = 0; + virtual void Unused5() = 0; + virtual void Unused6() = 0; + virtual void Unused7() = 0; + virtual bool GetPlayerInfo(int entNum, PlayerInfo *info) = 0; + virtual int GetPlayerForUserID(int userId) = 0; + virtual void *TextMessageGet(const char *name) = 0; + virtual bool Con_IsVisible() = 0; + virtual int GetLocalPlayer() = 0; +}; + +using CreateInterfaceFn = void *(*)(const char *name, int *returnCode); + +constexpr const char *GameEventManagerVersion = "GAMEEVENTSMANAGER002"; +constexpr const char *VEngineClientVersion = "VEngineClient014"; +constexpr int EventDebugIdInit = 42; + +static CreateInterfaceFn s_engineFactory; +static IGameEventManager2 *s_gameEventManager; +static IVEngineClient *s_engineClient; + +class ClientGameEventListener final : public IGameEventListener2 +{ +public: + void FireGameEvent(IGameEvent *event) override + { + if (!event || !s_clientGC || !s_engineClient) + { + return; + } + + const char *eventName = event->GetName(); + if (!strcmp(eventName, "round_start")) + { + int localPlayer = s_engineClient->GetLocalPlayer(); + if (localPlayer <= 0) + { + return; + } + + PlayerInfo playerInfo{}; + if (!s_engineClient->GetPlayerInfo(localPlayer, &playerInfo) || playerInfo.userId <= 0) + { + return; + } + + Platform::Print("Listener syncing local music kit state on round_start: userid=%d\n", playerInfo.userId); + s_clientGC->m_gc.PostToGC(GCEvent::SyncLocalPlayerMusicKitState, + 0, + &playerInfo.userId, + sizeof(playerInfo.userId)); + return; + } + + if (strcmp(eventName, "round_mvp")) + { + return; + } + + int localPlayer = s_engineClient->GetLocalPlayer(); + if (localPlayer <= 0) + { + return; + } + + PlayerInfo playerInfo{}; + if (!s_engineClient->GetPlayerInfo(localPlayer, &playerInfo)) + { + return; + } + + if (event->GetInt("userid") != playerInfo.userId) + { + return; + } + + int musickitmvps = event->GetInt("musickitmvps"); + if (musickitmvps <= 0) + { + musickitmvps = static_cast(s_clientGC->m_gc.LocalPlayerMusicKitMVPsForRoundMVPEvent()); + if (musickitmvps > 0) + { + event->SetInt("musickitmvps", musickitmvps); + Platform::Print("Listener injected local musickitmvps into round_mvp: %d\n", musickitmvps); + } + } + + Platform::Print("Listener saw local round_mvp: userid=%d reason=%d musickitmvps=%d\n", + playerInfo.userId, + event->GetInt("reason"), + musickitmvps); + + s_clientGC->m_gc.PostToGC(GCEvent::SyncLocalPlayerMusicKitState, + 0, + &playerInfo.userId, + sizeof(playerInfo.userId)); + s_clientGC->m_gc.PostToGC(GCEvent::LocalPlayerRoundMVP, 0, nullptr, 0); + } + + int GetEventDebugID() override + { + return EventDebugIdInit; + } +}; + +class ServerRoundMVPEventListener final : public IGameEventListener2 +{ +public: + void FireGameEvent(IGameEvent *event) override + { + if (!event || !s_serverGC || strcmp(event->GetName(), "round_mvp")) + { + return; + } + + int userId = event->GetInt("userid"); + if (userId <= 0) + { + return; + } + + int musickitmvps = 0; + if (!s_serverGC->m_gc.RoundMVPMusicKitCountForUserId(userId, musickitmvps)) + { + return; + } + + event->SetInt("musickitmvps", musickitmvps); + Platform::Print("Server listener injected musickitmvps into round_mvp: userid=%d musickitmvps=%d\n", + userId, + musickitmvps); + } + + int GetEventDebugID() override + { + return EventDebugIdInit; + } +}; + +static ClientGameEventListener s_clientGameEventListener; +static ServerRoundMVPEventListener s_serverRoundMVPEventListener; +static bool s_clientRoundMVPListenerRegistered = false; +static bool s_clientRoundStartListenerRegistered = false; +static bool s_serverRoundMVPListenerRegistered = false; + +static void InitializeClientGameInterfaces() +{ + if (s_engineFactory) + { + return; + } + + s_engineFactory = reinterpret_cast(Platform::ModuleFactory("engine")); + if (!s_engineFactory) + { + Platform::Print("engine CreateInterface not found\n"); + return; + } + + s_gameEventManager = reinterpret_cast(s_engineFactory(GameEventManagerVersion, nullptr)); + s_engineClient = reinterpret_cast(s_engineFactory(VEngineClientVersion, nullptr)); +} + +static void UpdateGameEventListeners() +{ + InitializeClientGameInterfaces(); + + if (!s_gameEventManager) + { + return; + } + + // Keep client listener lifecycle tied to the local ClientGC so reconnects do not + // leave stale listeners behind in engine.dll's event manager. + if (s_clientGC) + { + if (!s_clientRoundMVPListenerRegistered) + { + if (s_gameEventManager->AddListener(&s_clientGameEventListener, "round_mvp", false)) + { + s_clientRoundMVPListenerRegistered = true; + Platform::Print("Registered round_mvp listener\n"); + } + else + { + Platform::Print("Failed to register round_mvp listener\n"); + } + } + + if (!s_clientRoundStartListenerRegistered) + { + if (s_gameEventManager->AddListener(&s_clientGameEventListener, "round_start", false)) + { + s_clientRoundStartListenerRegistered = true; + Platform::Print("Registered round_start listener\n"); + } + else + { + Platform::Print("Failed to register round_start listener\n"); + } + } + } + else + { + if (s_clientRoundMVPListenerRegistered || s_clientRoundStartListenerRegistered) + { + s_gameEventManager->RemoveListener(&s_clientGameEventListener); + if (s_clientRoundMVPListenerRegistered) + { + Platform::Print("Unregistered round_mvp listener\n"); + } + if (s_clientRoundStartListenerRegistered) + { + Platform::Print("Unregistered round_start listener\n"); + } + s_clientRoundMVPListenerRegistered = false; + s_clientRoundStartListenerRegistered = false; + } + } + + if (s_serverGC && !s_serverRoundMVPListenerRegistered) + { + if (s_gameEventManager->AddListener(&s_serverRoundMVPEventListener, "round_mvp", true)) + { + s_serverRoundMVPListenerRegistered = true; + Platform::Print("Registered server-side round_mvp listener\n"); + } + else + { + Platform::Print("Failed to register server-side round_mvp listener\n"); + } + } + else if (!s_serverGC && s_serverRoundMVPListenerRegistered) + { + s_gameEventManager->RemoveListener(&s_serverRoundMVPEventListener); + s_serverRoundMVPListenerRegistered = false; + Platform::Print("Unregistered server-side round_mvp listener\n"); + } +} + template inline bool InterfaceMatches(const char *name, const char (&compare)[N]) { @@ -1864,6 +2171,8 @@ static void Hk_SteamAPI_RunCallbacks() { Og_SteamAPI_RunCallbacks(); + UpdateGameEventListeners(); + if (s_clientGC) { std::vector events; @@ -1930,6 +2239,8 @@ static void Hk_SteamGameServer_RunCallbacks() { Og_SteamGameServer_RunCallbacks(); + UpdateGameEventListeners(); + if (s_serverGC) { // only run server gc when logged on as an attempt to more accurately mimic real gc behaviour