/** * vim: set ts=4 : * ============================================================================= * SourceMod * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. * ============================================================================= * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 3.0, as published by the * Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . * * As a special exception, AlliedModders LLC gives you permission to link the * code of this program (as well as its derivative works) to "Half-Life 2," the * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software * by the Valve Corporation. You must obey the GNU General Public License in * all respects for all other code used. Additionally, AlliedModders LLC grants * this exception to all derivative works. AlliedModders LLC defines further * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), * or . * * Version: $Id$ */ #include "NextMap.h" #include "Logger.h" #include "HalfLife2.h" #include "sourcemm_api.h" #include "sm_stringutil.h" #include "logic_bridge.h" #include "compat_wrappers.h" #include #include NextMapManager g_NextMap; KHook::Virtual gCommandDispatch(&ConCommand::Dispatch, CmdChangeLevelCallback, nullptr); ConCommand *changeLevelCmd = NULL; ConVar sm_nextmap("sm_nextmap", "", FCVAR_NOTIFY); ConVar sm_maphistory_size("sm_maphistory_size", "20"); bool g_forcedChange = false; void NextMapManager::OnSourceModAllInitialized_Post() { m_HookChangeLevel.Add(engine); ConCommand *pCmd = FindCommand("changelevel"); if (pCmd != NULL) { gCommandDispatch.Add(pCmd); changeLevelCmd = pCmd; } } void NextMapManager::OnSourceModShutdown() { m_HookChangeLevel.Remove(engine); if (changeLevelCmd != NULL) { gCommandDispatch.Remove(changeLevelCmd); } auto iter = m_mapHistory.begin(); while (iter != m_mapHistory.end()) { delete (MapChangeData *)*iter; iter = m_mapHistory.erase(iter); } } const char *NextMapManager::GetNextMap() { return sm_nextmap.GetString(); } bool NextMapManager::SetNextMap(const char *map) { if (!g_HL2.IsMapValid(map)) { return false; } sm_nextmap.SetValue(map); return true; } static char g_nextMap[PLATFORM_MAX_PATH]; #if SOURCE_ENGINE != SE_DARKMESSIAH KHook::Return NextMapManager::HookChangeLevel(IVEngineServer* this_ptr, const char *map, const char *unknown) #else KHook::Return NextMapManager::HookChangeLevel(IVEngineServer* this_ptr, const char *map, const char *unknown, const char *video, bool bLongLoading) #endif { if (g_forcedChange) { logger->LogMessage("[SM] Changed map to \"%s\"", map); return { KHook::Action::Ignore }; } const char *newmap = sm_nextmap.GetString(); if (newmap[0] != '\0') { ke::SafeStrcpy(g_nextMap, sizeof(g_nextMap), newmap); newmap = g_nextMap; // Clear the value so that if the map load fails later we don't get stuck in a loop. // This might cause us to go off-cycle for a map, but nextmap will get us back on track. sm_nextmap.SetValue(""); } if (newmap[0] == '\0' || !g_HL2.IsMapValid(newmap)) { return { KHook::Action::Ignore }; } logger->LogMessage("[SM] Changed map to \"%s\"", newmap); ke::SafeStrcpy(m_tempChangeInfo.m_mapName, sizeof(m_tempChangeInfo.m_mapName), newmap); ke::SafeStrcpy(m_tempChangeInfo.m_changeReason, sizeof(m_tempChangeInfo.m_changeReason), "Normal level change"); #if SOURCE_ENGINE != SE_DARKMESSIAH return KHook::Recall(KHook::Return{ KHook::Action::Ignore }, this_ptr, newmap, unknown); #else return KHook::Recall(KHook::Return{ KHook::Action::Ignore }, this_ptr, newmap, unknown, video, bLongLoading); #endif } void NextMapManager::OnSourceModLevelChange( const char *mapName ) { // If we were controlling the map change, reset sm_nextmap to be the name of the map we successfully changed to. // This maintains an old API contract on the plugin side. We use the real map name even if it was different from // the expected map name as if the expected map failed to load we let the game take over instead, but the nextmap // plugin compares the sm_nextmap value to the current map to decide if it should advance the mapcycle. if (g_nextMap[0] != '\0') { sm_nextmap.SetValue(mapName); g_nextMap[0] = '\0'; } /* Skip the first 'mapchange' when the server starts up */ if (m_tempChangeInfo.startTime != 0) { if (strcmp(mapName, m_tempChangeInfo.m_mapName) == 0) { /* The map change was as we expected */ m_mapHistory.push_back(new MapChangeData(lastMap, m_tempChangeInfo.m_changeReason, m_tempChangeInfo.startTime)); } else { /* Something intercepted the mapchange */ char newReason[255]; ke::SafeSprintf(newReason, sizeof(newReason), "%s (Map overridden)", m_tempChangeInfo.m_changeReason); m_mapHistory.push_back(new MapChangeData(lastMap, newReason, m_tempChangeInfo.startTime)); } int historydiff = sm_maphistory_size.GetInt(); if (historydiff > 0) { historydiff -= m_mapHistory.size(); } else if (historydiff < 0) { historydiff = (m_mapHistory.size() * -1); } for (auto iter = m_mapHistory.begin(); historydiff++ < 0; iter = m_mapHistory.erase(iter)) { delete (MapChangeData *)*iter; } } m_tempChangeInfo.m_mapName[0] ='\0'; m_tempChangeInfo.m_changeReason[0] = '\0'; m_tempChangeInfo.startTime = time(NULL); ke::SafeStrcpy(lastMap, sizeof(lastMap), mapName); } void NextMapManager::ForceChangeLevel( const char *mapName, const char* changeReason ) { /* Store the mapname and reason */ ke::SafeStrcpy(m_tempChangeInfo.m_mapName, sizeof(m_tempChangeInfo.m_mapName), mapName); ke::SafeStrcpy(m_tempChangeInfo.m_changeReason, sizeof(m_tempChangeInfo.m_changeReason), changeReason); /* Change level and skip our hook */ g_forcedChange = true; engine->ChangeLevel(mapName, NULL); g_forcedChange = false; } NextMapManager::NextMapManager() : m_HookChangeLevel(&IVEngineServer::ChangeLevel, this, &NextMapManager::HookChangeLevel, nullptr) { m_tempChangeInfo = MapChangeData(); } #if SOURCE_ENGINE >= SE_ORANGEBOX KHook::Return CmdChangeLevelCallback(ConCommand*, const CCommand &command) { #else KHook::Return CmdChangeLevelCallback(ConCommand*) { CCommand command; #endif if (command.ArgC() < 2) { return { KHook::Action::Ignore }; } if (g_NextMap.m_tempChangeInfo.m_mapName[0] == '\0') { ke::SafeStrcpy(g_NextMap.m_tempChangeInfo.m_mapName, sizeof(g_NextMap.m_tempChangeInfo.m_mapName), command.Arg(1)); ke::SafeStrcpy(g_NextMap.m_tempChangeInfo.m_changeReason, sizeof(g_NextMap.m_tempChangeInfo.m_changeReason), "changelevel Command"); } return { KHook::Action::Ignore }; }