Beispiel #1
0
/*
 * 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;
    }
}