1143 lines
39 KiB
Plaintext
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);
|
||
|
}
|