TM2020-Gamemodes/TM_KnockoutDelayedCountdown...

1143 lines
39 KiB
Plaintext

/**
* Official Knockout mode with delayed countdown (Finish Timeout)
*/
#Extends "Libs/Nadeo/TMNext/TrackMania/Modes/TMNextRoundsBase.Script.txt"
#Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race"
#Const Version "2022-04-04"
#Const ScriptName "Modes/TrackMania/TM_Knockout_Online.Script.txt"
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Libraries
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
#Include "TextLib" as TL
#Include "MathLib" as ML
#Include "Libs/Nadeo/CommonLibs/Common/Semver.Script.txt" as Semver
#Include "Libs/Nadeo/ModeLibs/Common/Utils.Script.txt" as ModeUtils
#Include "Libs/Nadeo/TMNext/TrackMania/Modes/Knockout/StateManager.Script.txt" as StateMgr
#Include "Libs/Nadeo/TMNext/TrackMania/Menu/Constants.Script.txt" as MenuConsts
#Include "ManiaApps/Nadeo/TMNext/TrackMania/Knockout/UIModules/KnockoutInfo_Server.Script.txt" as UIModules_KnockoutInfo
#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable
#Include "ManiaApps/Nadeo/TMNext/TrackMania/TimeAttack/UIModules/BestRaceViewer_Server.Script.txt" as UIModules_BestRaceViewer
#Include "ManiaApps/Nadeo/TMNext/TrackMania/Knockout/UIModules/KnockedOutPlayers_Server.Script.txt" as UIModules_KnockedOutPlayers
#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage
#Include "ManiaApps/Nadeo/TMNext/TrackMania/Knockout/UIModules/KnockoutReward_Server.Script.txt" as UIModules_KnockoutReward
#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online
#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Settings
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
#Setting S_FinishTimeout 5 as _("Finish timeout")
#Setting S_RoundsPerMap -1 as _("Number of rounds per map") ///< Number of round to play on one map before going to the next one
#Setting S_WarmUpNb 0 as _("Number of warm up")
#Setting S_WarmUpDuration 0 as _("Duration of one warm up")
#Setting S_WarmUpTimeout -1 as _("Warm up timeout")
#Setting S_ChatTime 6
//L16N [Knockout] Setting for the KO mode, for exemple if the setting is "4,16,16" it means : "4 players eliminated per round until 16 players are left (16 is written twice, so two extra players are eliminated), then 2 players eliminated per round until 4 players are left (one extra player), then 1 player eliminated per round (default)".
#Setting S_EliminatedPlayersNbRanks "4,16,16" as _("Nb of players above which one extra elim. /round")
#Setting S_RoundsWithoutElimination 1 as _("Rounds without elimination")
#Setting S_EarlyEndMatchCallback True as "<hidden>"
#Setting S_MatchPosition -1 as "<hidden>" // Use to display player's final rank in CotD
#Setting S_FinishTimeoutPercent 100 as "Percent of finisher to start finish timeout"
#Setting S_FinishTimeoutPercentOnSurvivors True as "Apply percentage only on survivors"
/* About S_EliminatedPlayersNbRanks
* Example : "8,16"
* 1 to 8 players -> 1 elimination per round
* 9 to 16 players -> 2 eliminations per round
* 17 or more players -> 3 eliminations per round
*
* Example : "8,16,16"
* 1 to 8 players -> 1 eliminations per round
* 9 to 16 players -> 2 eliminations per round
* 17 or more players -> 4 eliminations per round
*
* Example : "0,8"
* 1 to 8 players -> 2 eliminations per round
* 9 or more players -> 3 eliminations per round
*
* Example : ""
* 1 or more players -> 1 elimination per round
*/
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Constants
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
#Const C_ModeName "Knockout"
//L16N [Knockout] Description of the mode
#Const Description _("$zIn $<$t$6F9Knockout$> mode, the goal is to be the last player standing. \n\nYou play a series of races as in Round mode. $<$t$6F9At the end of each race, the last players are eliminated$>!\n\nThe winner is the player who eliminates all of their opponents.")
#Const C_HudModulePath "" //< Path to the hud module
#Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/TMNext/TrackMania/Knockout/Knockout.Script.txt" //< Url of the mania app
#Const C_FakeUsersNb 0
#Const C_Callback_Elimination "Trackmania.Knockout.Elimination"
// [Knockout] Time remaining before the first round begins (in seconds). %1 is a marker to apply typography. %2 is a digit. e.g. "Next round in 3s"
#Const C_Text_NextRoundTimer _("%1Next round in %2s")
#Const C_UploadRecord True
#Const C_DisplayRecordGhost False
#Const C_DisplayRecordMedal False
#Const C_CelebrateRecordGhost True
#Const C_CelebrateRecordMedal True
#Struct K_MatchInfo {
Boolean RegistrationClosed;
Integer[Text] PlayerRanks; // from 1 to ParticipantsNb, 0 means still alive, -1 means player never played but mode tried to eliminate them
Integer KOPlayersNb;
Integer ParticipantsNb;
}
#Struct K_Callback_Elimination {
Text[] accountids;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Extends
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
***Match_LogVersions***
***
Log::RegisterScript(ScriptName, Version);
Log::RegisterScript(Semver::ScriptName, Semver::Version);
Log::RegisterScript(ModeUtils::ScriptName, ModeUtils::Version);
Log::RegisterScript(StateMgr::ScriptName, StateMgr::Version);
***
***Match_LoadLibraries***
***
// Not used for now but can be useful for players
XmlRpc::RegisterCallback(C_Callback_Elimination, """
* Name: {{{C_Callback_Elimination}}}
* Type: CallbackArray
* Description: Callback sent at the end of each round with the accountid of eliminated players.
* Data:
- Version >=2.0.0:
```
[
"{
"accountids": [
{
"45b9cf1e-3c97-4753-ac63-ac61b48b4bb7"
}
]
}"
]
```
""");
StateMgr::Load();
***
***Match_UnloadLibraries***
***
StateMgr::Unload();
XmlRpc::UnregisterCallback(C_Callback_Elimination);
***
***Match_Settings***
***
MB_Settings_UseDefaultHud = (C_HudModulePath == "");
MB_Settings_UseDefaultPodiumSequence = False;
***
***Match_Rules***
***
ModeInfo::SetName(C_ModeName);
ModeInfo::SetType(ModeInfo::C_Type_FreeForAll);
ModeInfo::SetRules(Description);
ModeInfo::SetStatusMessage("");
***
***Match_LoadHud***
***
if (C_HudModulePath != "") Hud_Load(C_HudModulePath);
***
***Match_AfterLoadHud***
***
ClientManiaAppUrl = C_ManiaAppUrl;
Race::SortScores(Race::C_Sort_TotalPoints);
UIModules_TimeGap::SetTimeGapMode(UIModules_TimeGap::C_TimeGapMode_CurRace);
UIModules_PauseMenu_Online::SetHelp(Description);
UIModules_Checkpoint::SetVisibilityTimeDiff(False);
UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_CurrentRace);
***
***Match_Yield***
***
foreach (Event in PendingEvents) {
switch (Event.Type) {
// Initialize players when they join the server
case CSmModeEvent::EType::OnPlayerAdded: {
StateMgr::InitializePlayer(Event.Player);
CarRank::InitializePlayer(Event.Player);
}
}
}
// Manage XmlRpc events
foreach (Event in XmlRpc.PendingEvents) {
if (Event.Type == CXmlRpcEvent::EType::CallbackArray) {
if (Event.ParamArray1 == "Club.Match.Start") { //@TODO @QG
declare Boolean Match_SkipWarmup for This = False;
Match_SkipWarmup = True;
} else if (Event.ParamArray1 == "Club.Match.Completed") { //@TODO @QG
declare Boolean Match_SkipWarmup for This = False;
Match_SkipWarmup = False;
}
}
}
StateMgr::Yield();
***
***Match_StartServer***
***
// Initialize mode
Clans::SetClansNb(0);
Scores::SaveInScore(Scores::C_Points_Match);
StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]);
WarmUp::SetAvailability(True);
Race::SetupRecord(
MenuConsts::C_ScopeType_Season,
MenuConsts::C_ScopeType_PersonalBest,
MenuConsts::C_GameMode_Knockout,
"",
C_UploadRecord,
C_DisplayRecordGhost,
C_DisplayRecordMedal,
C_CelebrateRecordGhost,
C_CelebrateRecordMedal
);
Race::UseAutomaticDossardColor(False);
***
***Match_InitMatch***
***
declare Integer Match_CurrentRoundNb;
declare Boolean Match_MatchIsOver;
declare Text[] Match_WinnersLogins;
declare Text[] Match_WinnersAccountIds;
declare Boolean Match_SkipWarmup for This = False; // Must survive CServerPlugin.RestartMap()
***
***Match_StartMatch***
***
Match_CurrentRoundNb = 0;
Match_WinnersLogins = [];
Match_WinnersAccountIds = [];
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
Server_MatchInfo = K_MatchInfo {};
OpenNewRegistrations();
foreach(Player in Players) {
if (Player != Null) RegisterPlayer(Player);
}
UpdateCustomRanking();
***
***Match_InitMap***
***
declare Boolean Map_DisplayCustom321Go;
UpdateScoresTableFooter(Match_CurrentRoundNb, GetTotalRoundNb(Match_CurrentRoundNb, GetAlivePlayers()));
UpdateCustomRanking();
UIModules_KnockoutInfo::UpdateLiveRanking();
UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_PrevTime);
***
***Match_StartMap***
***
Map_DisplayCustom321Go = True;
// Add bot when necessary
//Users_SetNbFakeUsers(C_FakeUsersNb, 0);
CarRank::Reset();
// Warm up
Race::SortScores(Race::C_Sort_BestRaceTime);
UIModules_ScoresTable::SetCustomRanks([]);
UIModules_ScoresTable::DisplayOnly([]);
UIModules_ScoresTable::SetCustomPoints([]);
if (!Match_SkipWarmup) { // Warmup only before daily match starts
MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000);
}
UpdateCustomRanking();
***
***Match_InitRound***
***
declare Text[] Round_EliminatedPlayers = []; // accountids
declare Integer Round_EliminatedPlayersNb;
declare Boolean NeedInfoDisplayUpdate;
declare Integer NBFinished = 0;
declare Integer MinimumFinishers = 0;
***
***Rounds_CanSpawn***
***
// Register new players while registration are open
if (RegistrationsAreOpen()) {
foreach(Player in Players) {
if (!PlayerIsRegistered(Player.User.WebServicesUserId)) RegisterPlayer(Player);
}
}
Match_CurrentRoundNb += 1;
declare Integer AlivePlayersNb = GetAlivePlayers();
Round_EliminatedPlayersNb = GetEliminationsNb(AlivePlayersNb, Match_CurrentRoundNb);
if (Round_EliminatedPlayersNb <= 0) {
CarRank::ResetRanksColors();
} else {
CarRank::SetRanksColors([
1 => Race::C_DossardColor_Default,
AlivePlayersNb - Round_EliminatedPlayersNb + 1 => <0.7, 0., 0.>
]);
}
UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers());
declare ObjectiveMessage = "";
if (Round_EliminatedPlayersNb > 1) {
//L16N [Knockout] Announce the number of players that will be eliminated at the end of the round. %1 will be replaced by a number greater than 1. eg: "2 players will be eliminated".
ObjectiveMessage = TL::Compose(_("%1 players will be eliminated"), "$t$i$fff"^TL::ToText(Round_EliminatedPlayersNb));
} else if (Round_EliminatedPlayersNb == 1){
//L16N [Knockout] Announce the number of players that will be eliminated at the end of the round. %1 will be replaced by the digit 1. eg: "1 player will be eliminated".
ObjectiveMessage = TL::Compose(_("%1 player will be eliminated"), "$t$i$fff"^TL::ToText(Round_EliminatedPlayersNb));
} else {
//L16N [Knockout] Announce the number of players that will be eliminated at the end of the round. %1 will be replaced by a marker for Typography
ObjectiveMessage = TL::Compose(_("%1No elimination this round"), "$t$i$fff");
}
UIModules_BigMessage::SetMessage(ObjectiveMessage, 5500);
UIModules_BigMessage::SetOffset(<0., -40.>);
if (Map_DisplayCustom321Go) {
UIManager.UIAll.BigMessageSound = CUIConfig::EUISound::Default;
UIManager.UIAll.BigMessageSoundVariant = 0;
MB_Sleep(1000);
UIManager.UIAll.BigMessage = TL::Compose(C_Text_NextRoundTimer, "$i$t", "3");
MB_Sleep(1000);
UIManager.UIAll.BigMessage = TL::Compose(C_Text_NextRoundTimer, "$i$t", "2");
MB_Sleep(1000);
UIManager.UIAll.BigMessage = TL::Compose(C_Text_NextRoundTimer, "$i$t", "1");
MB_Sleep(1000);
UIManager.UIAll.BigMessage = "";
StartTime = Now + Race::C_SpawnDuration;
}
// Reset spawn permissions for players and spectators according to Rounds rules
foreach (Score in Scores) {
if (Score == Null) continue;
declare ModeRounds_CanSpawn for Score = True;
declare Knockout_SpawnPermissionRequested for Score = False;
Knockout_SpawnPermissionRequested = False;
if (MM_IsMatchServer()) {
declare Player <=> GetPlayer(Score.User.WebServicesUserId);
ModeRounds_CanSpawn = MM_PlayerIsAllowedToPlay(Player);
} else {
ModeRounds_CanSpawn = True;
}
}
// Check spawn permissions for players according to KO rules
foreach (Player in Players) {
if (
Player != Null &&
Player.Score != Null
) {
declare Knockout_SpawnPermissionRequested for Player.Score = False;
if (!Knockout_SpawnPermissionRequested) {
Knockout_SpawnPermissionRequested = True;
RequestSpawnPermission(Player);
NeedInfoDisplayUpdate = True;
}
}
}
if (NeedInfoDisplayUpdate) {
UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers());
NeedInfoDisplayUpdate = False;
}
***
***Match_StartRound***
***
// Update UI
UIModules_BestRaceViewer::SetPrevDisplay(False);
foreach (Score in Scores) {
SetFinishedRace(Score, False);
TagDNF(Score, False);
TagAlive(Score, PlayerIsAlive(Score.User.WebServicesUserId));
}
UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers());
NeedInfoDisplayUpdate = False;
StateMgr::ForcePlayersStates([StateMgr::C_State_Playing]);
// Define minimum finishers
NBFinished = 0;
declare Integer Minus = 0;
if (S_FinishTimeoutPercentOnSurvivors) {
Minus = Round_EliminatedPlayersNb;
}
MinimumFinishers = ML::Max(1, ML::FloorInteger((GetAlivePlayers() - Minus) * (ML::ToReal(S_FinishTimeoutPercent) / 100)));
***
***Rounds_PlayerSpawned***
***
CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace);
***
***Rounds_PlayLoopSpawnPlayers***
***
// Spawn allowed players
foreach (Player in Players) {
if (
Player != Null &&
Player.Score != Null
) {
declare Knockout_SpawnPermissionRequested for Player.Score = False;
if (!Knockout_SpawnPermissionRequested) {
Knockout_SpawnPermissionRequested = True;
RequestSpawnPermission(Player);
NeedInfoDisplayUpdate = True;
}
declare ModeRounds_CanSpawn for Player.Score = True;
if (
Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned &&
MB_RoundIsRunning() &&
ModeRounds_CanSpawn &&
Race::IsReadyToStart(Player)
) {
Race::Start(Player, StartTime);
ModeRounds_CanSpawn = False;
TagDNF(Player.Score, False);
+++Rounds_PlayerSpawned+++
}
}
}
if (NeedInfoDisplayUpdate) {
UpdateKnockoutInfoDisplay(Match_CurrentRoundNb, Round_EliminatedPlayersNb, GetAlivePlayers());
NeedInfoDisplayUpdate = False;
}
***
***Match_PlayLoop***
***
foreach (Event in PendingEvents) {
if (Event.Type == CSmModeEvent::EType::OnPlayerAdded) {
if (Event.Player != Null) {
UIModules_BestRaceViewer::SetPrevDisplay(Event.Player, False);
}
}
}
// Manage race events
declare RacePendingEvents = Race::GetPendingEvents();
foreach (Event in RacePendingEvents) {
Race::ValidEvent(Event);
// Waypoint
if (Event.Type == Events::C_Type_Waypoint) {
CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace);
if (Event.Player != Null) {
if (Event.IsEndRace) {
Scores::UpdatePlayerPrevRace(Event.Player);
declare BetterRace = Scores::UpdatePlayerBestRaceIfBetter(Event.Player);
declare BetterLap = Scores::UpdatePlayerBestLapIfBetter(Event.Player);
// Start the countdown if enough players finished
NBFinished += 1 ;
if (EndTime <= 0 && (MinimumFinishers <= NBFinished)) {
EndTime = GetFinishTimeout();
}
SetFinishedRace(Event.Player.Score, True);
UIModules_BestRaceViewer::SetPrevDisplay(Event.Player, True);
UpdateCustomRanking();
}
if (Event.IsEndLap) {
declare Better = Scores::UpdatePlayerBestLapIfBetter(Event.Player);
}
}
UIModules_KnockoutInfo::UpdateLiveRanking();
}
// GiveUp
if (Event.Type == Events::C_Type_GiveUp) {
if (Event.Player != Null && Event.Player.Score != Null) {
TagDNF(Event.Player.Score, True);
}
UIModules_KnockoutInfo::UpdateLiveRanking();
}
}
// Manage mode events
foreach (Event in PendingEvents) {
if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue;
Events::Invalid(Event);
}
// Trigger Endtime if some players DC, go as Spectator or Give Up
if (Now % 5000 == 0) {
if (EndTime <= 0 && MinimumFinishers <= GetAlivePlayers() - PlayersNbAlive) {
EndTime = GetFinishTimeout();
}
}
***
***Match_EndRound***
***
Race::StopSkipOutroAll();
EndTime = -1;
StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]);
CarRank::Update(CarRank::C_SortCriteria_CurrentRace);
if (Semver::Compare(XmlRpc::GetApiVersion(), ">=", "2.1.1")) {
Scores::XmlRpc_SendScores(Scores::C_Section_PreEndRound, "");
}
if (Round_ForceEndRound || Round_SkipPauseRound) {
// Cancel points
foreach (Score in Scores) {
Scores::SetPlayerRoundPoints(Score, 0);
}
// Do not launch the forced end round sequence after a pause
if (!Round_SkipPauseRound) {
ForcedEndRoundSequence();
}
} else {
// Eliminate players that did not finish in time
declare Ident[] EliminatedPlayersScoresIds = []; // Score.Id
if (Match_CurrentRoundNb > S_RoundsWithoutElimination) {
// Close registrations. Players can join the game any time during no elimination rounds
if (RegistrationsAreOpen()) CloseRegistrations();
// Eliminate last players
Race::SortScores(Race::C_Sort_PrevRaceTime);
declare EliminationsNb = GetEliminationsNb(GetAlivePlayers(), Match_CurrentRoundNb);
declare Ident[] ReversedEliminatedPlayersScoresIds;
if (Scores.count > 0) {
for (I, 0, Scores.count-1) {
declare Score <=> Scores[Scores.count-1 - I];
if (Score != Null) {
if (PlayerIsAlive(Score.User.WebServicesUserId)) {
if (!GetFinishedRace(Score)) {
ReversedEliminatedPlayersScoresIds.add(Score.Id);
EliminationsNb -= 1;
TagDNF(Score, True);
} else if (EliminationsNb > 0) {
ReversedEliminatedPlayersScoresIds.add(Score.Id);
EliminationsNb -= 1;
}
}
}
}
foreach (ScoreId in ReversedEliminatedPlayersScoresIds) {
EliminatedPlayersScoresIds.add(ScoreId);
if (
Scores.existskey(ScoreId) &&
Scores[ScoreId] != Null
) {
Round_EliminatedPlayers.add(Scores[ScoreId].User.WebServicesUserId);
}
}
}
EliminatePlayers(EliminatedPlayersScoresIds);
}
// Check if match is over
if (MatchIsOver(Match_CurrentRoundNb)) {
Match_MatchIsOver = True;
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
Match_WinnersAccountIds = [];
Match_WinnersLogins = [];
foreach (AccountId => Status in Server_MatchInfo.PlayerRanks) {
if (Status == 0) Match_WinnersAccountIds.add(AccountId);
}
if (Match_WinnersAccountIds.count > 0) {
declare Ident[] LeaderScoresIds;
foreach (Score in Scores) {
if (
Score != Null &&
Match_WinnersAccountIds.exists(Score.User.WebServicesUserId)
) {
LeaderScoresIds.add(Score.Id);
Match_WinnersLogins.add(Score.User.Login);
}
}
EliminatePlayers(LeaderScoresIds); // Only one player will be considered as winner
}
}
// Send Trophies, uses Round_EliminatedPlayers array
+++Match_EndRound_AfterComputeScores+++
UIModules_KnockoutInfo::UpdateLiveRanking();
UpdateCustomRanking();
UpdateScoresTableFooter(Match_CurrentRoundNb, GetTotalRoundNb(Match_CurrentRoundNb, GetAlivePlayers()));
UIModules_KnockedOutPlayers::DisplayContent(True);
UIModules_KnockedOutPlayers::DisplayEliminatedPlayer(Round_EliminatedPlayers, GetPlayerRanks(Round_EliminatedPlayers));
declare PagesToShow = Round_EliminatedPlayers.count/4;
if (Round_EliminatedPlayers.count%4 != 0) PagesToShow += 1;
MB_Sleep(ML::Max(1100 + 350*Round_EliminatedPlayers.count + 1600*PagesToShow + 250, 6000 + 250));
UIModules_KnockedOutPlayers::DisplayEliminatedPlayer([], []);
UIModules_KnockedOutPlayers::DisplayContent(False);
declare K_Callback_Elimination Callback_Elimination;
foreach(AccountId in Round_EliminatedPlayers) {
Callback_Elimination.accountids.add(AccountId);
}
XmlRpc::SendCallback(C_Callback_Elimination, [Callback_Elimination.tojson()]);
foreach (ScoreId in EliminatedPlayersScoresIds) {
if (
Scores.existskey(ScoreId) &&
Scores[ScoreId] != Null
) {
declare Player = GetPlayer(Scores[ScoreId].User.Login);
if (Player != Null) {
UIModules_KnockoutReward::SetPlayerEliminated(Player, True);
UIModules_KnockoutReward::SendResult(Player);
}
}
}
foreach (Login in Match_WinnersLogins) {
declare Player = GetPlayer(Login);
if (Player != Null) {
UIModules_KnockoutReward::SetPlayerEliminated(Player, True); // Winner
UIModules_KnockoutReward::SendResult(Player);
}
}
if (MapIsOver(Match_CurrentRoundNb)) MB_StopMap();
}
***
***Match_EndMap***
***
if (MatchIsOver(Match_CurrentRoundNb)) {
MB_StopMatch();
} else {
MB_Sleep(2500);
}
if (!MB_MapIsRunning() && MB_MatchIsRunning()) MB_SkipPodiumSequence();
Race::SortScores(Race::C_Sort_TotalPoints);
Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_MatchPoints));
***
***Match_PodiumSequence***
***
if (!MB_Private_SkipPodiumSequence) {
ModeUtils::PlaySound(CUIConfig::EUISound::EndRound, 0);
declare WinnerScore <=> Scores::GetPlayerWinner();
if (WinnerScore != Null) {
UIModules_BigMessage::SetMessage(_("$<%1$> wins the match!"), WinnerScore.User.WebServicesUserId);
} else {
UIModules_BigMessage::SetMessage(_("|Match|Draw"));
}
// Send the EndMatch callback sooner to speed up the API update
if (S_EarlyEndMatchCallback) {
Scores::EndMatch();
Scores::XmlRpc_SendScores(Scores::C_Section_EndMatch, ""); // send "Trackmania.Scores"
Log::Log("Send early end match callback");
}
declare PrevUISequence = UIManager.UIAll.UISequence;
UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium;
MB_Private_Sleep(10000);
UIModules_BigMessage::SetMessage("");
UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
MB_Private_Sleep((S_ChatTime*1000));
UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
UIManager.UIAll.UISequence = PrevUISequence;
}
***
***Match_AfterPodiumSequence***
***
if (!MB_Private_SkipPodiumSequence) {
// End Match and delete data
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
Server_MatchInfo = K_MatchInfo {};
foreach (Score in Scores) {
if (Score == Null) continue;
TagDNF(Score, False);
TagAlive(Score, False);
UIModules_KnockoutReward::ResetResult(Score);
}
Scores::Clear();
UpdateScoresTableFooter(Match_CurrentRoundNb, GetTotalRoundNb(Match_CurrentRoundNb, GetAlivePlayers()));
UpdateCustomRanking();
UIModules_KnockoutInfo::UpdateLiveRanking();
foreach (Player in AllPlayers) {
if (Player != Null) {
UIModules_KnockoutReward::SetPlayerEliminated(Player, False);
}
}
}
***
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
// Functions
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Whether the player is registered or not
*
*@return True if the player is registered
* False Otherwise
*/
Boolean PlayerIsRegistered(Text _AccountId) {
if (_AccountId == "") return False;
declare K_MatchInfo Server_MatchInfo for This;
return Server_MatchInfo.PlayerRanks.existskey(_AccountId);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Whether the player has been eliminated or not
*
*@return True if the player has been eliminated
* False Otherwise
*/
Boolean PlayerIsAlive(Text _AccountId) {
if (_AccountId == "") return False;
declare K_MatchInfo Server_MatchInfo for This;
return (
PlayerIsRegistered(_AccountId) &&
Server_MatchInfo.PlayerRanks[_AccountId] == 0
);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Tag a score as finished curr race or not
*
* @param _Score
* @param _IsAlive
*/
Void TagAlive(CSmScore _Score, Boolean _IsAlive) {
if (_Score == Null) return;
declare netwrite Boolean Net_Knockout_PlayerIsAlive for _Score = False;
Net_Knockout_PlayerIsAlive = _IsAlive;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Tag a score as DNF
*
* @param _Score
* @param _DNF
*/
Void TagDNF(CSmScore _Score, Boolean _DNF) {
if (_Score == Null) return;
declare netwrite Boolean Net_Knockout_DNF for _Score = False;
Net_Knockout_DNF = _DNF;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Number of players still playing
*
* @return Number of players still playing
*/
Integer GetAlivePlayers() {
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
return (Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Init player at the begining of the match
*
* @param _Player Player to init
*/
Void RegisterPlayer(CSmPlayer _Player) {
if (_Player == Null) return;
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
if (Server_MatchInfo.RegistrationClosed) return;
if (
!Server_MatchInfo.PlayerRanks.existskey(_Player.User.WebServicesUserId) &&
!Server_MatchInfo.RegistrationClosed
) {
Server_MatchInfo.PlayerRanks[_Player.User.WebServicesUserId] = 0;
Server_MatchInfo.ParticipantsNb += 1;
UIModules_KnockoutInfo::SetAlivePlayersNb(GetAlivePlayers());
TagAlive(_Player.Score, True);
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Players can join the match after this function is called
*
*/
Void OpenNewRegistrations() {
declare K_MatchInfo Server_MatchInfo for This;
Server_MatchInfo = K_MatchInfo {
RegistrationClosed = False,
PlayerRanks = [],
KOPlayersNb = 0,
ParticipantsNb = 0
};
foreach (Score in Scores) {
UIModules_KnockoutReward::ResetResult(Score);
}
foreach (Player in AllPlayers) {
if (Player != Null) {
UIModules_KnockoutReward::SetPlayerEliminated(Player, False);
}
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Whether the player can join match or not
*
*@return True if the player can join
* False Otherwise
*/
Boolean RegistrationsAreOpen() {
declare K_MatchInfo Server_MatchInfo for This;
return !Server_MatchInfo.RegistrationClosed;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Players can't join the match anymore after this function is called
*
*/
Void CloseRegistrations() {
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
Server_MatchInfo.RegistrationClosed = True;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Eliminate players and assign them their final ranks
* according to the _ScoreIds list order. 1st player is the best ranked
*
* @param _ScoreIds Score Ids of Players to Eliminate
*/
Void EliminatePlayers(Ident[] _ScoreIds) {
if (_ScoreIds.count == 0) return;
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
if (!Server_MatchInfo.RegistrationClosed) return; // Cannot eliminate players if registrations are still open
foreach (ScoreId in _ScoreIds) {
if (Scores.existskey(ScoreId)) {
declare Score <=> Scores[ScoreId];
declare AccountId = Score.User.WebServicesUserId;
if (!PlayerIsRegistered(AccountId)) {
Server_MatchInfo.PlayerRanks[AccountId] = -1;
UIModules_KnockoutReward::SaveRank(Score, -1);
UIModules_KnockoutReward::SaveCupRank(Score, -1, -1);
} else if (PlayerIsAlive(AccountId)) {
Server_MatchInfo.KOPlayersNb += 1;
Server_MatchInfo.PlayerRanks[AccountId] = Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1;
UIModules_KnockoutReward::SaveRank(Score, Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1);
UIModules_KnockoutReward::SaveCupRank(Score, S_MatchPosition, Server_MatchInfo.ParticipantsNb - Server_MatchInfo.KOPlayersNb + 1);
}
TagAlive(Score, False);
}
}
}
Integer[] GetEliminationsMilestones() {
declare Text_Milestones = TL::Split(",", S_EliminatedPlayersNbRanks);
declare Integer[] Milestones = [1];
foreach (Text_Milestone in Text_Milestones) {
declare Milestone = TL::ToInteger(Text_Milestone);
if (Milestone > 0) Milestones.add(Milestone);
else if (Milestone == 0) Milestones.add(1);
}
return Milestones;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Players Nb at which Emilinations Nb per round decreases
*
* @param _AlivePlayers Number of players still playing
* @return Players Nb of the milestone
*/
Integer GetNextMilestone(Integer _AlivePlayers) {
if (_AlivePlayers <= 0) return 0;
declare Integer[] Milestones = GetEliminationsMilestones();
declare NextMilestone = 0;
foreach (Milestone in Milestones) {
if (Milestone < _AlivePlayers) {
NextMilestone = Milestone;
}
}
return NextMilestone;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Number of players to eliminate this round
*
* @param _AlivePlayers Number of players still playing
* @return Number of eliminations this round
*/
Integer GetEliminationsNb(Integer _AlivePlayers, Integer _RoundNb) {
if (_AlivePlayers <= 1) return 0;
if (_RoundNb <= S_RoundsWithoutElimination) return 0;
declare Milestones = GetEliminationsMilestones();
declare RoundMinEliminations = Milestones.count + 1;
foreach (Index => Milestone in Milestones) {
if (Milestone < _AlivePlayers) {
RoundMinEliminations = Index + 1;
}
}
return ML::Min(RoundMinEliminations, _AlivePlayers-1);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Estimated number of rounds
*
* @return Estimated number of rounds
*/
Integer GetTotalRoundNb(Integer _CurrentRoundNb, Integer _AlivePlayers) {
declare RoundsWithEliminationsLeft = 0;
declare AlivePlayers = _AlivePlayers;
declare Milestones = GetEliminationsMilestones();
for (Index, 0, Milestones.count-1) {
declare ReverseIndex = Milestones.count-1 - Index;
declare Milestone = Milestones[ReverseIndex];
if (AlivePlayers > Milestone && AlivePlayers > 1) {
declare ElimNbPerRound = ReverseIndex + 1;
declare TotalElimNb = AlivePlayers - Milestone;
if (TotalElimNb % ElimNbPerRound > 0) TotalElimNb += ElimNbPerRound - TotalElimNb % ElimNbPerRound;
RoundsWithEliminationsLeft += TotalElimNb / ElimNbPerRound;
AlivePlayers -= TotalElimNb;
}
}
return ML::Max(_CurrentRoundNb, S_RoundsWithoutElimination + 1) + ML::Max(0, RoundsWithEliminationsLeft -1);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Whether the player finished the race or not
*
* @param _Score Player's score
* @param _HasFinished True if player finished race
*/
Void SetFinishedRace(CSmScore _Score, Boolean _HasFinished) {
declare Knockout_FinishedRace for _Score = False;
Knockout_FinishedRace = _HasFinished;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Whether the player finished the race or not
*
* @param _Score Player's score
* @return True if player finished race
*/
Boolean GetFinishedRace(CSmScore _Score) {
declare Knockout_FinishedRace for _Score = False;
return Knockout_FinishedRace;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Player rank in match
*
* @param _AccountIds Account Ids
* @return Ranks in same order
*/
Integer[] GetPlayerRanks(Text[] _AccountIds) {
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
declare Integer[] Result = [];
foreach (AccountId in _AccountIds) {
if (Server_MatchInfo.PlayerRanks.existskey(AccountId)) {
Result.add(Server_MatchInfo.PlayerRanks[AccountId]);
} else {
Result.add(-2);
}
}
return Result;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Whether the player can be spwaned or not, according to KO rules only
*
*@param _Player The Player
*@return True if the player can be spwaned
* False Otherwise
*/
Boolean RequestSpawnPermission(CSmPlayer _Player) {
if (_Player == Null || _Player.Score == Null) return False;
declare ModeRounds_CanSpawn for _Player.Score = True;
if (
RegistrationsAreOpen() &&
!PlayerIsRegistered(_Player.User.WebServicesUserId)
) {
RegisterPlayer(_Player);
}
ModeRounds_CanSpawn = (
ModeRounds_CanSpawn &&
PlayerIsAlive(_Player.User.WebServicesUserId)
);
return ModeRounds_CanSpawn;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Update the Scores Table with hidden custom points
*
*/
Void UpdateCustomRanking() {
declare Text[] AccountIdsToDisplay = [];
declare Text[Text] CustomRanks = [];
declare Text[][Text] CustomPoints = [];
declare Integer[Text] CustomTimes = [];
declare K_MatchInfo Server_MatchInfo for This = K_MatchInfo {};
Race::SortScores(Race::C_Sort_PrevRaceTime);
declare AlivePlayers = GetAlivePlayers();
foreach (Index => Score in Scores) {
if (Score == Null) continue;
if (PlayerIsRegistered(Score.User.WebServicesUserId)) {
AccountIdsToDisplay.add(Score.User.WebServicesUserId);
if (PlayerIsAlive(Score.User.WebServicesUserId)) {
Scores::SetPlayerMatchPoints(Score, Scores.count + Server_MatchInfo.ParticipantsNb - Index);
CustomRanks[Score.User.WebServicesUserId] = "-";
if (!GetFinishedRace(Score)) {
CustomTimes[Score.User.WebServicesUserId] = 0; //@ prev race is updated automatically at the moment (22/10/20) so we need to use this
}
} else {
declare FinalRank = -1;
if (Server_MatchInfo.PlayerRanks.existskey(Score.User.WebServicesUserId)) {
FinalRank = Server_MatchInfo.PlayerRanks[Score.User.WebServicesUserId];
Scores::SetPlayerMatchPoints(Score, Server_MatchInfo.ParticipantsNb - FinalRank);
CustomRanks[Score.User.WebServicesUserId] = ""^FinalRank;
} else {
Scores::SetPlayerMatchPoints(Score, 0);
CustomRanks[Score.User.WebServicesUserId] = "-";
}
//L16N [Knockout] Do not translate if K.O. is understandable. This is the player status.
CustomPoints[Score.User.WebServicesUserId] = [_("|Status|K.O."), "f00"];
if (FinalRank == 1) {
//L16N [Knockout]
CustomPoints[Score.User.WebServicesUserId] = [_("|Status|Winner"), "0f0"];
}
}
}
}
Race::SortScores(Race::C_Sort_TotalPoints);
UIModules_ScoresTable::SetCustomRanks(CustomRanks);
UIModules_ScoresTable::DisplayOnly(AccountIdsToDisplay); // Display only registered players
UIModules_ScoresTable::SetCustomPoints(CustomPoints);
UIModules_ScoresTable::SetCustomTimes(CustomTimes);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Get the time left to the players to finish the round after the first player
*
* @return The time left in ms
*/
Integer GetFinishTimeout() {
declare FinishTimeout = 0;
if (S_FinishTimeout >= 0) {
FinishTimeout = S_FinishTimeout * 1000;
} else {
FinishTimeout = 5000;
if (Map.TMObjective_IsLapRace && Race::GetLapsNb() > 0 && Map.TMObjective_NbLaps > 0) {
FinishTimeout += ((Map.TMObjective_AuthorTime / Map.TMObjective_NbLaps) * Race::GetLapsNb()) / 6;
} else {
FinishTimeout += Map.TMObjective_AuthorTime / 6;
}
}
return Now + FinishTimeout;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Check if we should go to the next match
*
* @param _RoundNb Current round number
* @return True if it is the case, false otherwise
*/
Boolean MatchIsOver(Integer _RoundNb) {
if (_RoundNb < S_RoundsWithoutElimination || RegistrationsAreOpen() || GetAlivePlayers() > 1) return False;
return True;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Check if we should go to the next map
*
* @param _RoundNb Current round number
* @return True if it is the case, false otherwise
*/
Boolean MapIsOver(Integer _RoundNb) {
if (MatchIsOver(_RoundNb)) return True;
if (S_RoundsPerMap > 0 && MB_GetRoundCount() >= S_RoundsPerMap) return True; //< There is a rounds limit and it is reached
return False;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
/** Update the scores table footer text
*
* @param _TotalRounds The estimated number of rounds
* @param _AlivePlayers The number of Alive Players
* @param _RoundKOs The number players to eliminate this round
* @param _Milestone Next number of players at which one less player is eliminated per round
*/
Void UpdateScoresTableFooter(Integer _CurrentRoundNb, Integer _TotalRoundsNb) {
//L16N [Knockout]
declare Message = "";
if (MatchIsOver(_CurrentRoundNb)) {
Message = _("Match is over");
} else {
declare AlivePlayersNb = GetAlivePlayers();
declare Milestone = GetNextMilestone(AlivePlayersNb);
declare CurrentRound = _CurrentRoundNb;
if (CurrentRound == 0) CurrentRound = 1;
declare TotalRoundsNb = _TotalRoundsNb;
if (CurrentRound > TotalRoundsNb) TotalRoundsNb = CurrentRound;
declare EliminationsNb = GetEliminationsNb(AlivePlayersNb, _CurrentRoundNb);
if (EliminationsNb == 0 || Milestone <= 1) {
if (EliminationsNb <= 0) {
if (AlivePlayersNb <= 1) {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 1 OR LESS "No K.O. this round" means that no players will be eliminated, KO stands for "Knockouts". \n is a new line.
Message = TL::Compose(_("Round %1/%2\n%3 Player racing\nNo K.O. this round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb);
} else {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 2 OR MORE "No K.O. this round" means that no players will be eliminated, KO stands for "Knockouts". \n is a new line.
Message = TL::Compose(_("Round %1/%2\n%3 Players racing\nNo K.O. this round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb);
}
} else if (EliminationsNb == 1) {
if (AlivePlayersNb <= 1) {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 1 OR LESS. "1 K.O. per round" is the number of eliminated players, KO stands for "Knockouts". \n is a new line.
Message = TL::Compose(_("Round %1/%2\n%3 Player\n1 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb);
} else {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 2 OR MORE. "1 K.O. per round" is the number of eliminated players, KO stands for "Knockouts". \n is a new line.
Message = TL::Compose(_("Round %1/%2\n%3 Players\n1 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb);
}
} else {
if (AlivePlayersNb <= 1) {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 1 OR LESS. %4 the number of eliminated players, stands for "Knockouts", always greater than 1. \n is a new line.
Message = TL::Compose(_("Round %1/%2\n%3 Player\n%4 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb, ""^EliminationsNb);
} else {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round, and is always 2 OR MORE. %4 the number of eliminated players, stands for "Knockouts", always greater than 1. \n is a new line.
Message = TL::Compose(_("Round %1/%2\n%3 Players\n%4 K.O. per round"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb, ""^EliminationsNb);
}
}
} else {
//L16N [Knockout] Scores table footer. %1 is the number of current round. %2 the total number of rounds. %3 the number of players playing the round (assumed greater than 1). %4 the number of eliminated players, stands for "Knockouts", greater than 1. %5 is the next number of players at which one less player is eliminated per round. \n is a new line.
Message = TL::Compose(_("Round %1/%2, %3 Players\n%4 K.O. per round until %5 players"), ""^CurrentRound, ""^TotalRoundsNb, ""^AlivePlayersNb, ""^EliminationsNb, ""^Milestone);
}
}
UIModules_ScoresTable::SetFooterInfo(Message);
}
Void UpdateKnockoutInfoDisplay(Integer _CurrentRoundNb, Integer _Round_EliminatedPlayersNb, Integer _AlivePlayers) {
declare TotalRoundsNb = GetTotalRoundNb(_CurrentRoundNb, _AlivePlayers);
UIModules_KnockoutInfo::SetAlivePlayersNb(_AlivePlayers);
UIModules_KnockoutInfo::SetMapRoundNb(ML::Min(S_RoundsPerMap, MB_GetRoundCount()), ML::Min(S_RoundsPerMap, TotalRoundsNb));
UIModules_KnockoutInfo::SetRoundNb(_CurrentRoundNb, TotalRoundsNb);
UIModules_KnockoutInfo::SetKOsNumber(_Round_EliminatedPlayersNb, GetNextMilestone(_AlivePlayers));
UIModules_KnockoutInfo::UpdateLiveRanking();
UpdateCustomRanking();
UpdateScoresTableFooter(_CurrentRoundNb, TotalRoundsNb);
}