/* * Command line options for autostart (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; TYPEDIR is skirmishes, scenarios, or random * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI (0: sandbox, 5: very hard) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only) * -autostart-aiseed=AISEED sets the seed used for the AI random generator (default 0, use -1 for random) * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer game (default 2) * -autostart-client=IP sets multiplayer client to join host at given IP address * Random maps only: * -autostart-seed=SEED sets random map SEED value (default 0, use -1 for random) * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia 02" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attrs(cx); scriptInterface.Eval("({})", &attrs); JS::RootedValue settings(cx); scriptInterface.Eval("({})", &settings); JS::RootedValue playerData(cx); scriptInterface.Eval("([])", &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Default seed is 0 u32 seed = 0; CStr seedArg = args.Get("autostart-seed"); if (!seedArg.empty()) { // Random seed value if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Seed", seed); // Random seed scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(cx); scriptInterface.Eval("({})", &player); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway scriptInterface.SetProperty(player, "Civ", std::string("athen")); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", std::string("maps/" + autoStartName)); scriptInterface.SetProperty(settings, "mapType", mapType); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] }: if (args.Has("autostart-ai")) { std::vector<CStr> aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-1, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } scriptInterface.Eval("({})", &player); } CStr name = aiArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "AI", std::string(name)); scriptInterface.SetProperty(player, "AIDiff", 3); scriptInterface.SetPropertyInt(playerData, playerID-1, player); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector<CStr> civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-1, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } scriptInterface.Eval("({})", &player); } int difficulty = civArgs[i].AfterFirst(":").ToInt(); scriptInterface.SetProperty(player, "AIDiff", difficulty); scriptInterface.SetPropertyInt(playerData, playerID-1, player); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector<CStr> civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-1, &player) || player.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } scriptInterface.Eval("({})", &player); } CStr name = civArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "Civ", std::string(name)); scriptInterface.SetPropertyInt(playerData, playerID-1, player); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); JS::RootedValue mpInitData(cx); scriptInterface.Eval("({isNetworked:true, playerAssignments:{}})", &mpInitData); scriptInterface.SetProperty(mpInitData, "attribs", attrs); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) { userName = args.Get("autostart-playername").FromUTF8(); } if (args.Has("autostart-host")) { InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); g_NetServer = new CNetServer(maxPlayers); g_NetServer->UpdateGameAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupConnection("127.0.0.1"); } else if (args.Has("autostart-client")) { InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; bool ok = g_NetClient->SetupConnection(ip); ENSURE(ok); } else { g_Game->SetPlayerID(1); g_Game->StartGame(&attrs, ""); LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); InitPs(true, L"page_session.xml", NULL, JS::UndefinedHandleValue); } return true; }
void LeagueOverseer::Event (bz_EventData *eventData) { switch (eventData->eventType) { case bz_eAllowFlagGrab: // This event is called each time a player attempts to grab a flag { bz_AllowFlagGrabData_V1* allowFlagGrabData = (bz_AllowFlagGrabData_V1*)eventData; std::string flagAbbr = allowFlagGrabData->flagType; int playerID = allowFlagGrabData->playerID; if (isPcProtectionEnabled()) // Is the server configured to protect against Pass Camping { // Check if the last capture was within the 'PC_PROTECTION_DELAY' amount of seconds if (LAST_CAP + getPcProtectionDelay() > bz_getCurrentTime()) { // Check to see if the flag being grabbed belongs to the team that just had their flag captured AND check // to see if someone not from the victim team grabbed it if ((getTeamTypeFromFlag(flagAbbr) == CAP_VICTIM_TEAM && bz_getPlayerTeam(playerID) != CAP_VICTIM_TEAM)) { // Disallow the flag grab if it's being grabbed by an enemy right after a flag capture allowFlagGrabData->allow = false; } } } } break; case bz_eAllowSpawn: // This event is called before a player respawns { bz_AllowSpawnData_V2* allowSpawnData = (bz_AllowSpawnData_V2*)eventData; int playerID = allowSpawnData->playerID; if (!pluginSettings.isGuestSpawningEnabled(getCurrentGameMode()) && !isLeagueMember(playerID)) { // Disable their spawning privileges allowSpawnData->handled = true; allowSpawnData->allow = false; allowSpawnData->kickPlayer = false; sendPluginMessage(playerID, pluginSettings.getGuestSpawningMessage(getCurrentGameMode())); } } break; case bz_eBZDBChange: // This event is called each time a BZDB variable is changed { bz_BZDBChangeData_V1* bzdbData = (bz_BZDBChangeData_V1*)eventData; if (bzdbData->key == "_pcProtectionDelay") { // Save the proposed value in a variable for easy access int proposedValue = std::stoi(bzdbData->value.c_str()); // Our PC protection delay should be between 3 and 30 seconds only, otherwise set it to the default 5 seconds if (proposedValue < 3 || proposedValue > 30) { bz_setBZDBInt("_pcProtectionDelay", 5); } } } break; case bz_eCaptureEvent: // This event is called each time a team's flag has been captured { if (isMatchInProgress()) { bz_CTFCaptureEventData_V1* captureData = (bz_CTFCaptureEventData_V1*)eventData; std::shared_ptr<bz_BasePlayerRecord> capperData(bz_getPlayerByIndex(captureData->playerCapping)); // Keep score (captureData->teamCapping == TEAM_ONE) ? currentMatch.incrementTeamOneScore() : currentMatch.incrementTeamTwoScore(); // Store data for PC Protection CAP_VICTIM_TEAM = captureData->teamCapped; CAP_WINNER_TEAM = captureData->teamCapping; LAST_CAP = captureData->eventTime; logMessage(pluginSettings.getDebugLevel(), "debug", "%s captured the flag at %s", capperData->callsign.c_str(), getMatchTime().c_str()); logMessage(pluginSettings.getDebugLevel(), "debug", "Match Score %s [%i] vs %s [%i]", currentMatch.getTeamOneName().c_str(), currentMatch.getTeamOneScore(), currentMatch.getTeamTwoName().c_str(), currentMatch.getTeamTwoScore()); CaptureMatchEvent capEvent = CaptureMatchEvent().setBZID(capperData->bzID.c_str()) .setTime(getMatchTime()); // If it's an official match, save the team ID if (isOfficialMatch()) { int teamID = (captureData->teamCapping == TEAM_ONE) ? currentMatch.getTeamOneID() : currentMatch.getTeamTwoID(); capEvent.setTeamID(teamID); } capEvent.save(); currentMatch.saveEvent(capEvent.getJsonObject()); currentMatch.stats_flagCapture(captureData->playerCapping); logMessage(pluginSettings.getVerboseLevel(), "debug", "CaptureMatchEvent JSON -> %s", capEvent.toString()); } } break; case bz_eGameEndEvent: // This event is called each time a game ends { logMessage(pluginSettings.getVerboseLevel(), "debug", "A match has ended."); // Get the current standard UTC time bz_Time stdTime; bz_getUTCtime(&stdTime); std::string recordingFileName; // Only save the recording buffer if we actually started recording when the match started if (RECORDING) { logMessage(pluginSettings.getVerboseLevel(), "debug", "Recording was in progress during the match."); std::stringstream recordingName; std::string _matchType = (isOfficialMatch()) ? "offi" : "fm", _teamOneName = currentMatch.getTeamOneName(), _teamTwoName = currentMatch.getTeamTwoName(); // (offi|fm)-YYYYMMDD recordingName << _matchType << "-" << stdTime.year << formatInt("%02d", stdTime.month) << formatInt("%02d", stdTime.day); if (!currentMatch.isRosterEmpty()) { std::replace(_teamOneName.begin(), _teamOneName.end(), ' ', '_'); std::replace(_teamTwoName.begin(), _teamTwoName.end(), ' ', '_'); // Team_A-vs-Team_B recordingName << "-" << _teamOneName << "-vs-" << _teamTwoName; } // -HHMM recordingName << "-" << formatInt("%02d", stdTime.hour) << formatInt("%02d", stdTime.minute); const std::string nameForHash = recordingName.str(); std::string hash = bz_MD5(nameForHash.c_str()); // -ACBD123 recordingName << "-" << hash.substr(0, 7); if (currentMatch.matchCanceled()) { // -Canceled recordingName << "-Canceled"; } recordingName << ".rec"; recordingFileName = recordingName.str(); logMessage(pluginSettings.getVerboseLevel(), "debug", "Replay file will be named: %s", recordingFileName.c_str()); // Save the recording buffer and stop recording bz_saveRecBuf(recordingFileName.c_str(), 0); bz_stopRecBuf(); logMessage(pluginSettings.getVerboseLevel(), "debug", "Replay file has been saved and recording has stopped."); // We're no longer recording, so set the boolean and announce to players that the file has been saved RECORDING = false; bz_sendTextMessagef(BZ_SERVER, BZ_ALLUSERS, "Match saved as: %s", recordingFileName.c_str()); } // Format the date to -> year-month-day hour:minute:second char matchDate[20]; sprintf(matchDate, "%02d-%02d-%02d %02d:%02d:%02d", stdTime.year, stdTime.month, stdTime.day, stdTime.hour, stdTime.minute, stdTime.second); currentMatch.save(matchDate, recordingFileName); if (pluginSettings.isMatchReportEnabled()) { if (!currentMatch.isRosterEmpty()) { MatchUrlRepo.set("query", "matchReport") .set("data", bz_urlEncode(currentMatch.toString().c_str())) .submit(); if (currentMatch.isFM()) { // It was a fun match, so there is no need to do anything logMessage(pluginSettings.getVerboseLevel(), "debug", "Fun match has completed."); } else if (currentMatch.matchCanceled()) { // The match was canceled for some reason so output the reason to both the players and the server logs logMessage(pluginSettings.getDebugLevel(), "debug", "%s", currentMatch.getCancelation().c_str()); bz_sendTextMessage(BZ_SERVER, BZ_ALLUSERS, currentMatch.getCancelation().c_str()); } else { logMessage(pluginSettings.getDebugLevel(), "debug", "Reporting match data..."); bz_sendTextMessage(BZ_SERVER, BZ_ALLUSERS, "Reporting match..."); // Send the match data to the league website MATCH_INFO_SENT = true; } } } // Reset our match data currentMatch = Match(); // Empty our list of players since we don't need a history activePlayerList.clear(); } break; case bz_eGamePauseEvent: { bz_GamePauseResumeEventData_V2* gamePauseData = (bz_GamePauseResumeEventData_V2*)eventData; // Get the current UTC time MATCH_PAUSED = time(NULL); // Send the messages bz_sendTextMessagef(BZ_SERVER, BZ_ALLUSERS, " with %s remaining.", getMatchTime().c_str()); logMessage(pluginSettings.getVerboseLevel(), "debug", "Match paused at %s by %s.", getMatchTime().c_str(), bz_getPlayerCallsign(gamePauseData->playerID)); PauseResumeMatchEvent pauseEvent = PauseResumeMatchEvent(); pauseEvent.setState(true) .setTime(getMatchTime()) .setBZID(getPlayerBZID(gamePauseData->playerID)) .save(); currentMatch.saveEvent(pauseEvent.getJsonObject()); logMessage(pluginSettings.getVerboseLevel(), "debug", "PauseResumeMatchEvent JSON -> %s", pauseEvent.toString()); } break; case bz_eGameResumeEvent: { bz_GamePauseResumeEventData_V2* gameResumeData = (bz_GamePauseResumeEventData_V2*)eventData; // Get the current UTC time time_t now = time(NULL); // Do the math to determine how long the match was paused double timePaused = difftime(now, MATCH_PAUSED); // Create a temporary variable to store and manipulate the match start time struct tm modMatchStart = *localtime(&MATCH_START); // Manipulate the time by adding the amount of seconds the match was paused in order to be able to accurately // calculate the amount of time remaining with LeagueOverseer::getMatchTime() modMatchStart.tm_sec += timePaused; // Save the manipulated match start time MATCH_START = mktime(&modMatchStart); logMessage(pluginSettings.getVerboseLevel(), "debug", "Match paused for %.f seconds. Match continuing at %s.", timePaused, getMatchTime().c_str()); PauseResumeMatchEvent resumeEvent = PauseResumeMatchEvent(); resumeEvent.setState(false) .setTime(getMatchTime()) .setBZID(getPlayerBZID(gameResumeData->playerID)) .save(); currentMatch.saveEvent(resumeEvent.getJsonObject()); logMessage(pluginSettings.getVerboseLevel(), "debug", "PauseResumeMatchEvent JSON -> %s", resumeEvent.toString()); } break; case bz_eGameStartEvent: // This event is triggered when a timed game begins { logMessage(pluginSettings.getVerboseLevel(), "debug", "A match has started"); // Empty our list of players since we don't need a history activePlayerList.clear(); // We started recording a match, so save the status RECORDING = bz_startRecBuf(); // We want to notify the logs if we couldn't start recording just in case an issue were to occur and the server // owner needs to check to see if players were lying about there no replay if (RECORDING) { logMessage(pluginSettings.getVerboseLevel(), "debug", "Match recording has started successfully"); } else { logMessage(0, "error", "This match could not be recorded"); } currentMatch.setMatchDuration((int) bz_getTimeLimit()); MATCH_START = time(NULL); } break; case bz_eGetAutoTeamEvent: // This event is called for each new player is added to a team { bz_GetAutoTeamEventData_V1* autoTeamData = (bz_GetAutoTeamEventData_V1*)eventData; int playerID = autoTeamData->playerID; std::shared_ptr<bz_BasePlayerRecord> playerData(bz_getPlayerByIndex(playerID)); // Only force new players to observer if a match is in progress if (isMatchInProgress()) { // Automatically move non-league members or players who just joined to the observer team if (!isLeagueMember(playerID) || !playerAlreadyJoined(playerData->bzID.c_str())) { autoTeamData->handled = true; autoTeamData->team = eObservers; } } } break; case bz_eGetPlayerMotto: // This event is called when the player joins. It gives us the motto of the player { bz_GetPlayerMottoData_V2* mottoData = (bz_GetPlayerMottoData_V2*)eventData; if (pluginSettings.isMottoFetchEnabled()) { std::map<std::string, std::string> parameters; parameters["{motto}"] = mottoData->motto; parameters["{team}"] = getPlayerTeamNameByBZID(mottoData->record->bzID.c_str()); mottoData->motto = formatMotto(parameters); } } break; case bz_ePlayerDieEvent: // This event is called each time a tank is killed { bz_PlayerDieEventData_V1* dieData = (bz_PlayerDieEventData_V1*)eventData; std::shared_ptr<bz_BasePlayerRecord> victimData(bz_getPlayerByIndex(dieData->playerID)); std::shared_ptr<bz_BasePlayerRecord> killerData(bz_getPlayerByIndex(dieData->killerID)); if (isMatchInProgress()) { KillMatchEvent killEvent = KillMatchEvent(); killEvent.setKiller(killerData->bzID.c_str()) .setVictim(victimData->bzID.c_str()) .setTime(getMatchTime()) .save(); currentMatch.saveEvent(killEvent.getJsonObject()); logMessage(pluginSettings.getVerboseLevel(), "debug", "KillMatchEvent JSON -> %s", killEvent.toString()); } } break; case bz_ePlayerJoinEvent: // This event is called each time a player joins the game { bz_PlayerJoinPartEventData_V1* joinData = (bz_PlayerJoinPartEventData_V1*)eventData; int playerID = joinData->playerID; std::shared_ptr<bz_BasePlayerRecord> playerData(bz_getPlayerByIndex(playerID)); setLeagueMember(playerID); storePlayerInfo(playerID, playerData->bzID.c_str()); JoinMatchEvent joinEvent = JoinMatchEvent().setCallsign(playerData->callsign.c_str()) .setVerified(playerData->verified) .setIpAddress(playerData->ipAddress.c_str()) .setBZID(playerData->bzID.c_str()); joinEvent.save(); // Only notify a player if they exist, have joined the observer team, and there is a match in progress if (isMatchInProgress() && isValidPlayerID(joinData->playerID) && playerData->team == eObservers) { bz_sendTextMessagef(BZ_SERVER, joinData->playerID, "*** There is currently %s match in progress, please be respectful. ***", ((isOfficialMatch()) ? "an official" : "a fun")); } if (pluginSettings.isMottoFetchEnabled()) { // Only send a URL job if the user is verified if (playerData->verified) { requestTeamName(playerData->callsign.c_str(), playerData->bzID.c_str()); } } } break; case bz_ePlayerPartEvent: // This event is called each time a player leaves a game { bz_PlayerJoinPartEventData_V1* partData = (bz_PlayerJoinPartEventData_V1*)eventData; int playerID = partData->playerID; std::shared_ptr<bz_BasePlayerRecord> playerData(bz_getPlayerByIndex(playerID)); removePlayerInfo(partData->record->bzID.c_str()); // Only keep track of the parting player if they are a league member and there is a match in progress if (isLeagueMember(playerID) && isMatchInProgress()) { if (!playerAlreadyJoined(playerData->bzID.c_str())) { // Create a record for the player who just left Player partingPlayer(playerData->bzID.c_str(), playerData->team, bz_getCurrentTime()); // Push the record to our vector activePlayerList.push_back(partingPlayer); } } } break; case bz_eRawChatMessageEvent: // This event is called for each chat message the server receives. It is called before any filtering is done. { bz_ChatEventData_V1* chatData = (bz_ChatEventData_V1*)eventData; bz_eTeamType target = chatData->team; int playerID = chatData->from, recipient = chatData->to; // The server is always allowed to talk if (playerID == BZ_SERVER) { break; } // A non-league player is attempting to talk if (!isLeagueMember(playerID)) { std::string allowedChatTarget = pluginSettings.getAllowedTargetChat(getCurrentGameMode()); bool ignoreMessage = true; if (allowedChatTarget == "Observers") { if (recipient == eObservers || bz_getPlayerTeam(recipient) == eObservers) { ignoreMessage = false; } } else if (allowedChatTarget == "ObvserverAdmins") { if (bz_getPlayerTeam(recipient) == eObservers && isVisibleAdmin(recipient)) { ignoreMessage = false; } } else if (allowedChatTarget == "Admins") { if (target == eAdministrators || isVisibleAdmin(recipient)) { ignoreMessage = false; } } else if (allowedChatTarget == "All") { ignoreMessage = false; } if (ignoreMessage) { chatData->message = ""; sendPluginMessage(playerID, pluginSettings.getGuestMessagingMessage(getCurrentGameMode())); } } } break; case bz_eTickEvent: // This event is called once for each BZFS main loop { // Get the total number of tanks playing int totalTanks = bz_getTeamCount(eRedTeam) + bz_getTeamCount(eGreenTeam) + bz_getTeamCount(eBlueTeam) + bz_getTeamCount(ePurpleTeam); // If there are no tanks playing, then we need to do some clean up if (totalTanks == 0) { // If there is an official match and no tanks playing, we need to cancel it if (isOfficialMatch()) { currentMatch.cancelMatch("Official match automatically canceled due to all players leaving the match."); } // If we have players recorded and there's no one around, empty the list if (!activePlayerList.empty()) { activePlayerList.clear(); } // If there is a countdown active an no tanks are playing, then cancel it if (bz_isCountDownActive()) { bz_gameOver(253, eObservers); logMessage(pluginSettings.getVerboseLevel(), "debug", "Game ended because no players were found playing with an active countdown."); } } // Let's get the roll call only if there is an official match if (isOfficialMatch()) { // Check if the start time is not negative since our default value for the approxTimeProgress is -1. Also check // if it's time to do a roll call, which is defined as 90 seconds after the start of the match by default, // and make sure we don't have any match participants recorded and the match isn't paused if (getMatchProgress() > currentMatch.getMatchRollCall() && currentMatch.isRosterEmpty() && !bz_isCountDownPaused() && !bz_isCountDownInProgress()) { logMessage(pluginSettings.getVerboseLevel(), "debug", "Processing roll call..."); // @TODO Make sure all of these variables are used std::shared_ptr<bz_APIIntList> playerList(bz_getPlayerIndexList()); bool invalidateRollCall, teamOneError, teamTwoError; std::string teamOneMotto, teamTwoMotto; int teamOneID, teamTwoID; invalidateRollCall = teamOneError = teamTwoError = false; teamOneMotto = teamTwoMotto = ""; // We can't do a roll call if the player list wasn't created if (!playerList) { logMessage(pluginSettings.getVerboseLevel(), "error", "Failure to create player list for roll call."); return; } for (unsigned int i = 0; i < playerList->size(); i++) { bz_BasePlayerRecord* playerRecord = bz_getPlayerByIndex(playerList->get(i)); if (playerRecord && isLeagueMember(playerRecord->playerID) && bz_getPlayerTeam(playerList->get(i)) != eObservers) // If player is not an observer { currentMatch.savePlayer(playerRecord, getTeamIdFromBZID(playerRecord->bzID.c_str())); // @TODO Rewrite this function to be updated // Check if there is any need to invalidate a roll call from a team //validateTeamName(invalidateRollCall, teamOneError, currentPlayer, teamOneMotto, TEAM_ONE); //validateTeamName(invalidateRollCall, teamTwoError, currentPlayer, teamTwoMotto, TEAM_TWO); } bz_freePlayerRecord(playerRecord); } // We were asked to invalidate the roll call because of some issue so let's check if there is still time for // another roll call if (invalidateRollCall && currentMatch.incrementMatchRollCall(60) < currentMatch.getMatchDuration()) { logMessage(pluginSettings.getDebugLevel(), "debug", "Invalid player found on field at %s.", getMatchTime().c_str()); // There was an error with one of the members of either team, so request a team name update for all of // the team members to try to fix any inconsistencies of different team names //if (teamOneError) { requestTeamName(TEAM_ONE); } //if (teamTwoError) { requestTeamName(TEAM_TWO); } // Delay the next roll call by 60 seconds logMessage(pluginSettings.getVerboseLevel(), "debug", "Match roll call time has been delayed by 60 seconds."); // Clear the struct because it's useless data currentMatch.clearPlayerRoster(); logMessage(pluginSettings.getVerboseLevel(), "debug", "Match participants have been cleared."); } // There is no need to invalidate the roll call so the team names must be right so save them in the struct if (!invalidateRollCall) { currentMatch.setTeamOneID(teamOneID).setTeamOneName(teamOneMotto) .setTeamTwoID(teamTwoID).setTeamTwoName(teamTwoMotto); logMessage(pluginSettings.getVerboseLevel(), "debug", "Team One set to: %s", currentMatch.getTeamOneName().c_str()); logMessage(pluginSettings.getVerboseLevel(), "debug", "Team Two set to: %s", currentMatch.getTeamTwoName().c_str()); } } } } break; default: break; } }