diff --git a/src/multiaddonmanager.cpp b/src/multiaddonmanager.cpp index 87662a5..a47c802 100644 --- a/src/multiaddonmanager.cpp +++ b/src/multiaddonmanager.cpp @@ -141,6 +141,18 @@ struct ClientJoinInfo_t CUtlVector g_ClientsPendingAddon; // List of clients who are still downloading addons std::unordered_set g_ClientsWithAddons; // List of clients who already downloaded everything so they don't get reconnects on mapchange/rejoin +CConVar mm_extra_addons("mm_extra_addons", FCVAR_NONE, "The workshop IDs of extra addons separated by commas, addons will be downloaded (if not present) and mounted", CUtlString(""), + [](CConVar *cvar, CSplitScreenSlot slot, const CUtlString *new_val, const CUtlString *old_val) + { + g_ClientsWithAddons.clear(); + Message("Clearing client cache due to addons changing"); + + StringToVector(new_val->Get(), g_MultiAddonManager.m_ExtraAddons); + + g_MultiAddonManager.RefreshAddons(); + }); + + MultiAddonManager g_MultiAddonManager; INetworkGameServer *g_pNetworkGameServer = nullptr; CSteamGameServerAPIContext g_SteamAPI; @@ -291,9 +303,23 @@ bool MultiAddonManager::MountAddon(const char *pszAddon, bool bAddToTail = false if (!pszAddon || !*pszAddon) return false; + CUtlVector serverMountedAddons; + StringToVector(this->m_sCurrentWorkshopMap.c_str(), serverMountedAddons); + if (serverMountedAddons.Find(pszAddon) != -1) + { + Message("%s: Addon %s is already mounted by the server\n", __func__, pszAddon); + return false; + } + PublishedFileId_t iAddon = V_StringToUint64(pszAddon, 0); uint32 iAddonState = g_SteamAPI.SteamUGC()->GetItemState(iAddon); + if (iAddonState & k_EItemStateLegacyItem) + { + Message("%s: Addon %s is not compatible with Source 2, skipping\n", __func__, pszAddon); + return false; + } + if (!(iAddonState & k_EItemStateInstalled)) { Message("%s: Addon %s is not installed, queuing a download\n", __func__, pszAddon); @@ -427,7 +453,7 @@ bool MultiAddonManager::DownloadAddon(const char *pszAddon, bool bImportant = fa return true; } -void MultiAddonManager::RefreshAddons(bool bReloadMap = false) +void MultiAddonManager::RefreshAddons(bool bReloadMap) { if (!g_SteamAPI.SteamUGC()) return; @@ -454,6 +480,9 @@ void MultiAddonManager::ClearAddons() { m_ExtraAddons.RemoveAll(); + // Update the convar to reflect the new addon list, but don't trigger the callback + mm_extra_addons.GetConVarData()->Value(0)->m_StringValue = VectorToString(m_ExtraAddons).c_str(); + FOR_EACH_VEC_BACK(m_MountedAddons, i) UnmountAddon(m_MountedAddons[i].c_str()); } @@ -524,6 +553,9 @@ bool MultiAddonManager::AddAddon(const char *pszAddon, bool bRefresh = false) m_ExtraAddons.AddToTail(pszAddon); + // Update the convar to reflect the new addon list, but don't trigger the callback + mm_extra_addons.GetConVarData()->Value(0)->m_StringValue = VectorToString(m_ExtraAddons).c_str(); + g_ClientsWithAddons.clear(); Message("Clearing client cache due to addons changing"); @@ -547,6 +579,9 @@ bool MultiAddonManager::RemoveAddon(const char *pszAddon, bool bRefresh = false) m_ExtraAddons.Remove(index); + // Update the convar to reflect the new addon list, but don't trigger the callback + mm_extra_addons.GetConVarData()->Value(0)->m_StringValue = VectorToString(m_ExtraAddons).c_str(); + g_ClientsWithAddons.clear(); Message("Clearing client cache due to addons changing"); @@ -556,17 +591,6 @@ bool MultiAddonManager::RemoveAddon(const char *pszAddon, bool bRefresh = false) return true; } -CConVar mm_extra_addons("mm_extra_addons", FCVAR_NONE, "The workshop IDs of extra addons separated by commas, addons will be downloaded (if not present) and mounted", CUtlString(""), - [](CConVar *cvar, CSplitScreenSlot slot, const CUtlString *new_val, const CUtlString *old_val) - { - g_ClientsWithAddons.clear(); - Message("Clearing client cache due to addons changing"); - - StringToVector(new_val->Get(), g_MultiAddonManager.m_ExtraAddons); - - g_MultiAddonManager.RefreshAddons(); - }); - CON_COMMAND_F(mm_add_addon, "Add a workshop ID to the extra addon list", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY) { if (args.ArgC() < 2) @@ -694,29 +718,38 @@ bool FASTCALL Hook_SendNetMessage(CServerSideClient *pClient, CNetMessage *pData void FASTCALL Hook_SetPendingHostStateRequest(int numRequest, CHostStateRequest *pRequest) { + // When IVEngineServer::ChangeLevel is called by the plugin or the server code, + // (which happens at the end of a map), the server-defined addon does not change. + // Also, host state requests coming from that function will always have "ChangeLevel" in its KV. + // We can use this information to always be aware of what the original addon is. + + if (!pRequest->m_pKV) + { + g_MultiAddonManager.ClearCurrentWorkshopMap(); + } + else if (V_stricmp(pRequest->m_pKV->GetName(), "ChangeLevel")) + { + if (!V_stricmp(pRequest->m_pKV->GetName(), "map_workshop")) + g_MultiAddonManager.SetCurrentWorkshopMap(pRequest->m_pKV->GetString("customgamemode", "")); + else + g_MultiAddonManager.ClearCurrentWorkshopMap(); + } if (g_MultiAddonManager.m_ExtraAddons.Count() == 0) return g_pfnSetPendingHostStateRequest(numRequest, pRequest); - CUtlVector vecAddons; - StringToVector(pRequest->m_Addons.Get(), vecAddons); - - // Clear the string just in case it wasn't somehow, like when reloading the map - pRequest->m_Addons.Clear(); - - std::string sExtraAddonString = VectorToString(g_MultiAddonManager.m_ExtraAddons); - - // If it's empty or the first addon in the string is ours, it means we're on a default map - if (vecAddons.Count() == 0 || g_MultiAddonManager.m_ExtraAddons.HasElement(vecAddons[0])) + // Rebuild the addon list. We always start with the original addon. + if (g_MultiAddonManager.GetCurrentWorkshopMap().empty()) { - Message("%s: setting addon string to \"%s\"\n", __func__, sExtraAddonString.c_str()); - pRequest->m_Addons = sExtraAddonString.c_str(); - g_MultiAddonManager.ClearCurrentWorkshopMap(); + pRequest->m_Addons = VectorToString(g_MultiAddonManager.m_ExtraAddons).c_str(); } else { - Message("%s: appending \"%s\" to addon string \"%s\"\n", __func__, sExtraAddonString.c_str(), vecAddons[0].c_str()); - pRequest->m_Addons.Format("%s,%s", vecAddons[0].c_str(), sExtraAddonString.c_str()); - g_MultiAddonManager.SetCurrentWorkshopMap(vecAddons[0].c_str()); + // Don't add the same addon twice. Hopefully no server owner is diabolical enough to do things like `map de_dust2 customgamemode=1234,5678`. + CUtlVector newAddons; + newAddons.CopyArray(g_MultiAddonManager.m_ExtraAddons.Base(), g_MultiAddonManager.m_ExtraAddons.Count()); + newAddons.FindAndRemove(g_MultiAddonManager.GetCurrentWorkshopMap().c_str()); + newAddons.AddToHead(g_MultiAddonManager.GetCurrentWorkshopMap().c_str()); + pRequest->m_Addons = VectorToString(newAddons).c_str(); } g_pfnSetPendingHostStateRequest(numRequest, pRequest); diff --git a/src/multiaddonmanager.h b/src/multiaddonmanager.h index 3907c9d..5e50b15 100644 --- a/src/multiaddonmanager.h +++ b/src/multiaddonmanager.h @@ -59,7 +59,7 @@ class MultiAddonManager : public ISmmPlugin, public IMetamodListener bool RemoveAddon(const char *pszAddon, bool bRefresh); bool DownloadAddon(const char *pszAddon, bool bImportant, bool bForce); void PrintDownloadProgress(); - void RefreshAddons(bool bReloadMap); + void RefreshAddons(bool bReloadMap = false); void ClearAddons(); void ReloadMap(); std::string GetCurrentWorkshopMap() { return m_sCurrentWorkshopMap; } @@ -76,6 +76,7 @@ class MultiAddonManager : public ISmmPlugin, public IMetamodListener const char *GetLogTag(); CUtlVector m_ExtraAddons; + // List of addons mounted by the plugin. Does not contain the original server mounted addon. CUtlVector m_MountedAddons; private: CUtlVector m_ImportantDownloads; // Important addon downloads that will trigger a map reload when finished