/**************************************** * Athletics Game Mode * * Author: steeffeen * * Contact: steeffeen@team-devota.com * ****************************************/ /* TODO: - spawn screens in disciplines show current ranking OF THE discipline */ #Extends "Modes/ShootMania/ModeBase.Script.txt" #Include "MathLib" as MathLib #Include "TextLib" as TextLib #Include "Libs/Nadeo/Chrono.Script.txt" as Chrono #Include "Libs/Nadeo/Message.Script.txt" as Message #Include "Libs/Nadeo/TabsServer.Script.txt" as Tabs #Include "Libs/Nadeo/Top2.Script.txt" as Top #Include "Libs/Nadeo/ShootMania/AFK.Script.txt" as AFK #Include "Libs/Nadeo/ShootMania/Score.Script.txt" as Score #Include "Libs/Nadeo/ShootMania/ScoresTable.Script.txt" as ScoresTable #Include "Libs/Nadeo/ShootMania/SM.Script.txt" as SM #Include "Libs/Nadeo/ShootMania/SpawnScreen.Script.txt" as SpawnScreen #Const CompatibleMapTypes "AthleticsArena" #Const Version "0.6.9 (2014-03-23)" // SETTINGS #Setting S_TimeLimit 16 as _("Time Limit per Map (Minutes)") #Setting S_ManageAFKPlayers True as _("Force AFK-Players into Spectator") // CONSTANTS #Const C_NeutralEmblemUrl "" // Gameplay contants #Const C_PointsPerDiscipline 100. // Points granted to the winner of a certain discipline // TEXTS #Const Description _("Compete against other international $<$08fAthletes$> in different Disciplines!") #Const C_LobbyName "Lobby" // Name of lobby // Discipline types #Const C_DisciplineTypes [1 => "LongJump", 2 => "HighJump", 3 => "Run", 4 => "Rounds"] // GLOBALES declare Integer G_LastUIUpdate; // Time of last ui update declare Integer[Text] G_Disciplines; // Disciplines of current map declare Real[Text] G_DisciplineRecords; // Record per discipline declare Integer[Text] G_DisciplineCPCount; // Count of checkpoints of rounds based disciplines declare Integer[Text] G_DisciplineRoundsCount; // Amount of rounds to finish a specific rounds discipline declare Text[Ident] G_DisciplinePorts; // Ports of the disciplines declare Text[Ident] G_DisciplineGoals; // Goals of the disciplines declare CUILayer[Text] G_DisciplinesScoresLayers; // Layers of disciplines scores declare Boolean[Text] G_DisciplineScoresUpdated; // If scores of a disciplines has been updated declare Integer G_LastAFKCheck; // last time afk check was performed ***StartServer*** *** log("Athletics.Script.txt loaded!"); log("Version: "^Version); // Gameplay UseClans = False; UsePvPWeapons = False; UsePvPCollisions = False; MB_NeutralEmblemUrl = C_NeutralEmblemUrl; G_LastUIUpdate = 0; G_LastAFKCheck = 0; // UI UIManager.UIAll.AltMenuNoDefaultScores = True; UIManager.UIAll.AltMenuNoCustomScores = True; SM::SetupDefaultVisibility(); // UI Layers Chrono::Load(); Tabs::Load(); // Utility layer declare UtilityLayer <=> UIManager.UILayerCreate(); UtilityLayer.ManialinkPage = GetUtilityLayerManialink(); UIManager.UIAll.UILayers.add(UtilityLayer); // Markers layer declare MarkersLayer <=> UIManager.UILayerCreate(); MarkersLayer.Type = CUILayer::EUILayerType::Markers; UIManager.UIAll.UILayers.add(MarkersLayer); // Rules SpawnScreen::CreateRules("Athletics", """ - Achieve the best results in the different $<$05cDisciplines$> to win the tournament! - $<$08fRun$>, $<$08fjump$> and $<$08fskate$> to show your opponents who's the best $<$3afathlete$>! - Walk into the disciplines ports to start your attempts. - Press $<$0f0F3$> to return to the $<$f80{{{C_LobbyName}}}$>. """); SpawnScreen::CreateScores(); *** ***StartMap*** *** Message::SendBigMessage(_("New Match!"), 3000, 3, CUIConfig::EUISound::StartMatch, 0); // Initialize map InitMap(); foreach (Base in Bases) { Base.Clan = 0; Base.IsActive = True; } foreach (Pole in BlockPoles) { Pole.Captured = True; Pole.Gauge.Clan = 0; Pole.Gauge.ValueReal = 1.; } UIManager.UIAll.Hud3dMarkers = GetHud3dMarkers(); MarkersLayer.ManialinkPage = GetMarkersLayerManialink(); // Tabs declare CUILayer TabsLayer <=> CreateTabPaneLayer(GetDisciplinesImages(G_Disciplines), 25, 15, True); TabsLayer.Type = CUILayer::EUILayerType::AltMenu; UIManager.UIAll.UILayers.add(TabsLayer); // Scores table ScoresTable::Load(); ScoresTable::SetColumnsWidth(2., 2., 3., 19., 1.5, 1.5, 0., 0., 0., 0., 5.5); ScoresTable::SetColumnsName("", "", "", "", "Score"); ScoresTable::SetTableFormat(2, 8); ScoresTable::SetTableWidth(239.); ScoresTable::Build(); ScoresTable::GetLayerScoresTable().Type = CUILayer::EUILayerType::Normal; // Disciplines scores G_DisciplinesScoresLayers.clear(); foreach (Discipline => Type in G_Disciplines) { declare Layer <=> UIManager.UILayerCreate(); Layer.Type = CUILayer::EUILayerType::AltMenu; G_DisciplinesScoresLayers[Discipline] = Layer; UIManager.UIAll.UILayers.add(Layer); G_DisciplineScoresUpdated[Discipline] = True; } SpawnScreen::CreateMapInfo(); // Initialize scores Score::MatchBegin(); ScoresTable::StartMatch(); InitAllPlayersScores(True); // Start game StartTime = Now + 3000; EndTime = StartTime + S_TimeLimit * 60000; UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing; *** ***OnNewPlayer*** *** declare Text Discipline for Player; Discipline = ""; InitPlayerScore(Player.Score, False); UpdatePlayerScore(Player.Score); declare UI <=> UIManager.GetUI(Player); if (UI != Null) { Tabs::UseTabs(UI, "ScoresTab"); } UpdateAllDisciplinesScores(); *** ***OnNewSpectator*** *** declare UI <=> UIManager.GetUI(Spectator); if (UI != Null) { Tabs::UseTabs(UI, "ScoresTab"); } *** ***PlayLoop*** *** Message::Loop(); // Manage afk players if (S_ManageAFKPlayers) { if (G_LastAFKCheck + 30000 < Now) { G_LastAFKCheck = Now; AFK::ManageAFKPlayers(); } } // Actions for each player foreach (Player in Players) { switch (Player.SpawnStatus) { case CSmPlayer::ESpawnStatus::NotSpawned: { // Spawn the player MySpawnPlayer(Player); Chrono::Start(Player.Id, Player.StartTime-Now); } case CSmPlayer::ESpawnStatus::Spawned: { declare Discipline for Player = ""; switch (Discipline) { case "": { // In LOBBY if (Player.BlockPole != Null && G_DisciplinePorts.existskey(Player.BlockPole.Id)) { // Player stepped on discipline port Discipline = G_DisciplinePorts[Player.BlockPole.Id]; UpdateDiscipline(Player, Discipline); UnspawnPlayer(Player); } } default: { // In discipline switch (G_Disciplines[Discipline]) { case 1: { // Long jumping if (Player.IsTouchingGround) { // Check for jump length declare AirTimeBegin for Player = -1; if (AirTimeBegin >= 0) { AirTimeBegin = -1; declare Boolean ValidJump for Player = True; if (ValidJump) { declare Vec3 AirTimeBeginPosition for Player; declare Distance = MathLib::Distance(AirTimeBeginPosition, Player.Position); EvaluateTry(Player, Distance, False); } } } else { declare AirTimeBegin for Player = -1; if (AirTimeBegin < 0) { // Begin air time AirTimeBegin = Now; declare Vec3 AirTimeBeginPosition for Player; AirTimeBeginPosition = Player.Position; declare Boolean ValidJump for Player; ValidJump = IsPlayerInMapArea(Player); } } } case 2: { // High jumping declare LastHeight for Player = 0.; declare JumpStartHeight for Player = 0.; declare MaxJumpHeight for Player = 0.; declare HighJumpStart for Player = False; if (Player.IsTouchingGround) { // Track jump start height JumpStartHeight = Player.Position.Y; MaxJumpHeight = 0.; } else { // Check for jump height if (Player.Position.Y > LastHeight) { HighJumpStart = True; if (Player.Position.Y-JumpStartHeight > MaxJumpHeight) { // Height increased MaxJumpHeight = Player.Position.Y - JumpStartHeight; } } else { if (HighJumpStart) { // Evaluate try EvaluateTry(Player, MaxJumpHeight, False); MaxJumpHeight = 0.; } HighJumpStart = False; } } LastHeight = Player.Position.Y; } case 3: { // Run if (Player.BlockPole != Null && G_DisciplineGoals.existskey(Player.BlockPole.Id)) { if (Discipline == G_DisciplineGoals[Player.BlockPole.Id]) { // Player reached finish declare Time = Now - Player.StartTime; EvaluateTry(Player, Time / 1000., True); UnspawnPlayer(Player); } } } case 4: { // Rounds if (Player.BlockPole != Null && G_DisciplineGoals.existskey(Player.BlockPole.Id) && Discipline == G_DisciplineGoals[Player.BlockPole.Id]) { // Player reached cp declare RoundsStartId for Player = NullId; declare RoundsCPs for Player = Ident[]; declare RoundsCount for Player = 0; declare UI <=> UIManager.GetUI(Player); if (RoundsStartId == NullId) { // Try just started RoundsStartId = Player.BlockPole.Id; if (UI != Null) { UI.SendNotice( """Start: {{{(Now - Player.StartTime) / 1000.}}}""", CUIConfig::ENoticeLevel::Default, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Checkpoint, 4); } } else { if (Player.BlockPole.Id == RoundsStartId) { // Start reached again if (RoundsCPs.count+1 >= G_DisciplineCPCount[Discipline]) { // Round finished RoundsCount += 1; RoundsCPs.clear(); if (RoundsCount >= G_DisciplineRoundsCount[Discipline]) { // Run finished declare Time = Now - Player.StartTime; EvaluateTry(Player, Time / 1000., True); if (UI != Null) { UI.SendNotice( "", CUIConfig::ENoticeLevel::Default, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Finish, 0); } UnspawnPlayer(Player); } else { if (UI != Null) { UI.SendNotice( """Round {{{RoundsCount}}}/{{{G_DisciplineRoundsCount[Discipline]}}}: {{{(Now - Player.StartTime) / 1000.}}}""", CUIConfig::ENoticeLevel::Default, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Checkpoint, 1); } } } } else { // CP reached if (!RoundsCPs.exists(Player.BlockPole.Id)) { RoundsCPs.add(Player.BlockPole.Id); if (UI != Null) { UI.SendNotice( """CP {{{RoundsCPs.count}}}: {{{(Now-Player.StartTime)/1000.}}}""", CUIConfig::ENoticeLevel::Default, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Checkpoint, 1); } } } } } } } } } } } } // Handle events foreach (Event in PendingEvents) { switch (Event.Type) { case CSmModeEvent::EType::OnArmorEmpty: { if (Event.Victim == Null || Event.Shooter != Null) { Discard(Event); continue; } // Offzone declare Text Discipline for Event.Victim; switch (Discipline) { case "": { // In lobby } default: { // In discipline switch (G_Disciplines[Discipline]) { case 1: { // Long jumping - Check for jump length declare AirTimeBegin for Event.Victim = -1; if (AirTimeBegin >= 0) { AirTimeBegin = -1; declare ValidJump for Event.Victim = True; if (ValidJump) { declare Vec3 AirTimeBeginPosition for Event.Victim; declare Distance = MathLib::Distance(AirTimeBeginPosition, Event.Victim.Position); EvaluateTry(Event.Victim, Distance, False); } } } } } } UnspawnPlayer(Event.Victim); Discard(Event); } case CSmModeEvent::EType::OnHit: { Discard(Event); } default: { PassOn(Event); } } } // UI updates if (G_LastUIUpdate + 250 < Now) { G_LastUIUpdate = Now; // Update disciplines scores layers foreach (Disci => DisciLayer in G_DisciplinesScoresLayers) { if (G_DisciplineScoresUpdated.existskey(Disci) && G_DisciplineScoresUpdated[Disci]) { // Layer update needed DisciLayer.ManialinkPage = GetDisciplineRankingsLayer(Disci); declare Temp = G_DisciplineScoresUpdated.removekey(Disci); } } // Players foreach (Player in Players) { declare UI <=> UIManager.GetUI(Player); if (UI != Null) { // Update utility layer UpdateUtilityLayer(Player, UI); // Update discipline in UI declare Discipline for Player = ""; declare netwrite Net_Discipline for UI = ""; if (Net_Discipline != Discipline) { Net_Discipline = Discipline; } } } // Spectators foreach (Spectator in Spectators) { declare UI <=> UIManager.GetUI(Spectator); if (UI != Null) { // Update discipline in UI declare netwrite Text Net_Discipline for UI; if (Net_Discipline != "") { Net_Discipline = ""; } } } } // Map end conditions if (Now >= EndTime) { MB_StopMap = True; } *** ***EndMap*** *** Score::MatchEnd(); SendCallbackEndAthleticsMap(); MarkersLayer.ManialinkPage = ""; UIManager.UIAll.Hud3dMarkers = ""; foreach (Player in Players) { declare UI <=> UIManager.GetUI(Player); if (UI != Null) { declare netwrite Text Discipline for UI; Discipline = ""; Chrono::Destroy(Player.Id); } } declare CUser Winner <=> Null; declare Best = 0; foreach (Score in Scores) { if (Score.Points > Best) { Winner = Score.User; Best = Score.Points; } } declare Message = _("Match Draw"); if (Winner != Null) { Message = TextLib::Compose(_("%1 wins the Map!"), "$<"^Winner.Name^"$>"); } Message::SendBigMessage(Message, 3000, 3, CUIConfig::EUISound::EndMatch, 0); UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium; UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; MB_Sleep(4000); ScoresTable::EndMatch(); ScoresTable::Unload(); foreach (Layer in G_DisciplinesScoresLayers) { UIManager.UILayerDestroy(Layer); } UIManager.UILayerDestroy(TabsLayer); *** ***EndServer*** *** // UI cleanup Chrono::Unload(); SpawnScreen::DestroyRules(); SpawnScreen::DestroyScores(); SpawnScreen::DestroyMapInfo(); UIManager.UILayerDestroyAll(); *** // Return the unit for values of a discipline Text GetUnitForDiscipline(Text _Discipline) { if (G_Disciplines.existskey(_Discipline)) { switch (G_Disciplines[_Discipline]) { case 1: { return "m"; } case 2: { return "m"; } case 3: { return "s"; } case 4: { return "s"; } } } return ""; } // Convert boolean from maniascript to json Text ToJson(Boolean _Boolean) { if (_Boolean) return "true"; return "false"; } // Send callback to inform that a player started a discipline Void SendCallbackPlayerDisciplineStart(Text _Login, Text _Discipline, Boolean _Spectating) { declare Data = """{ "Login": "{{{_Login}}}", "Discipline": "{{{_Discipline}}}", "Unit": "{{{GetUnitForDiscipline(_Discipline)}}}", "Spectating": {{{ToJson(_Spectating)}}} }"""; XmlRpc.SendCallback("playerDisciplineStart", Data); } // Send callback to inform about an attempt Void SendCallbackPlayerDisciplineFinish(Text _Login, Text _Discipline, Real _Value) { declare Data = """{ "Login": "{{{_Login}}}", "Discipline": "{{{_Discipline}}}", "Value": {{{_Value}}}, "Unit": "{{{GetUnitForDiscipline(_Discipline)}}}" }"""; XmlRpc.SendCallback("playerDisciplineFinish", Data); } // Send callback to inform about the disciplines rankings at end of map Void SendCallbackEndAthleticsMap() { declare Text[] DisciplinesData; declare Data = """{ """; // Loop through disciplines foreach (Discipline => Type in G_Disciplines) { declare DisciplineData = """ "{{{Discipline}}}": { "Unit": "{{{GetUnitForDiscipline(Discipline)}}}", "Values": {"""; declare UseData = False; // Loop through player records of the discipline foreach (Score in Scores) { declare Real[Text] DisciplineRecords for Score; if (DisciplineRecords.existskey(Discipline)) { if (UseData) { DisciplineData ^= ","; } DisciplineData ^= """ "{{{Score.User.Login}}}": {{{DisciplineRecords[Discipline]}}}"""; UseData = True; } } // Only add discipline to json if there is data to pass if (UseData) { DisciplineData ^= """ }"""; DisciplinesData.add(DisciplineData); } } // Merge disciplines data declare Index = 0; foreach (DisciplineData in DisciplinesData) { Data ^= DisciplineData; if (Index < DisciplinesData.count) { Data ^= ","; } Index += 1; } Data ^= """ }"""; XmlRpc.SendCallback("endAthleticsMap", Data); } // Updates current discipline of a player Void UpdateDiscipline(CSmPlayer _Player, Text _Discipline) { declare Text Discipline for _Player; Discipline = _Discipline; if (!_Player.IsFakePlayer) { SendCallbackPlayerDisciplineStart(_Player.Login, Discipline, False); } // Announce discipline if (_Discipline == "") { Message::SendStatusMessage(_Player, "Lobby", 3000, 2); } else { Message::SendStatusMessage(_Player, Discipline, 3000, 2); } // Update spectating players as well foreach (Spectator in Spectators) { declare UI <=> UIManager.GetUI(Spectator); if (UI != Null) { declare netread Net_CurrentSpecTarget for UI = ""; if (Net_CurrentSpecTarget == _Player.Login) { SendCallbackPlayerDisciplineStart(Spectator.Login, Discipline, True); } } } } // Get discipline name of a goal Text GetDisciplineName(CSmBlockPole _Pole) { for (Index, 0, TextLib::Length(_Pole.Tag)-1) { if (TextLib::SubString(_Pole.Tag, Index, 1) == ":") { return TextLib::SubString(_Pole.Tag, 0, Index); } } return ""; } // Get discipline type of a goal Text GetGoalType(CSmBlockPole _Pole) { for (Index, 0, TextLib::Length(_Pole.Tag) - 1) { if (TextLib::SubString(_Pole.Tag, Index, 1) == ":") { return TextLib::SubString(_Pole.Tag, Index + 2, TextLib::Length(_Pole.Tag) - 2 - Index); } } return ""; } // Gather disciplines and stuff of the current map Void InitMap() { G_Disciplines.clear(); G_DisciplineRecords.clear(); G_DisciplinePorts.clear(); G_DisciplineGoals.clear(); G_DisciplineCPCount.clear(); G_DisciplineRoundsCount.clear(); // Gather disciplines foreach (Spawn in BlockSpawns) { if (Spawn.Tag != C_LobbyName && Spawn.Tag != "Spawn") { G_Disciplines[Spawn.Tag] = Spawn.Order; } } // Gather goals foreach (Name => Type in G_Disciplines) { // Gather ports foreach (Pole in BlockPoles) { if (GetDisciplineName(Pole) == Name) { switch (GetGoalType(Pole)) { case "Port": { G_DisciplinePorts[Pole.Id] = Name; } case "Goal": { G_DisciplineGoals[Pole.Id] = Name; } } } } // Gather specific goals switch (Type) { case 4: { // Rounds declare RoundsCount = 2; foreach (Pole in BlockPoles) { // Gather checkpoints if (GetDisciplineName(Pole) == Name && GetGoalType(Pole) == "Goal") { if (!G_DisciplineCPCount.existskey(Name)) { G_DisciplineCPCount[Name] = 0; } G_DisciplineCPCount[Name] += 1; } // Get rounds count if (Pole.Order > 0) { RoundsCount = Pole.Order; } } G_DisciplineRoundsCount[Name] = RoundsCount; } } } } // Initializes records Void InitPlayerScore(CSmScore _Score, Boolean _Forced) { if (_Score != Null) { declare RealNewPlayer for _Score = True; if (RealNewPlayer || _Forced) { declare Real[Text] DisciplineRecords for _Score; DisciplineRecords = Real[Text]; declare Real[Text] DisciplinePoints for _Score; DisciplinePoints = Real[Text]; RealNewPlayer = False; } } } Void InitAllPlayersScores(Boolean _Forced) { if (_Forced) { G_DisciplineRecords.clear(); } foreach (Score in Scores) { InitPlayerScore(Score, _Forced); } } // Get spawn of discipline CSmBlockSpawn GetDisciplineSpawn(Text _Discipline) { declare SpawnName = _Discipline; if (SpawnName == "") { SpawnName = "Lobby"; } foreach (Spawn in BlockSpawns) { if (Spawn.Tag == SpawnName) { return Spawn; } } return Null; } // Spawn player depending on discipline Void MySpawnPlayer(CSmPlayer _Player) { declare Discipline for _Player = ""; // Set default weapon SetPlayerWeapon(_Player, CSmMode::EWeapon::Rocket, True); // Reset discipline values switch (Discipline) { case "": { // Lobby Chrono::Destroy(_Player.Id); } default: { switch (G_Disciplines[Discipline]) { case 1: { // Long jumping declare Integer AirTimeBegin for _Player; AirTimeBegin = -1; Chrono::Destroy(_Player.Id); } case 2: { // High jumping Chrono::Destroy(_Player.Id); } case 3: { // Run Chrono::Create(_Player.Id); } case 4: { // Rounds declare Ident RoundsStartId for _Player; RoundsStartId = NullId; declare Ident[] RoundsCPs for _Player; RoundsCPs = Ident[]; declare Integer RoundsCount for _Player; RoundsCount = 0; Chrono::Create(_Player.Id); } } } } // Spawn player at discipline spawn SM::SpawnPlayer(_Player, 0, GetDisciplineSpawn(Discipline)); } // Updates score of a player depending on his performances in the different disciplines Void UpdatePlayerScore(CSmScore _Score) { if (_Score != Null) { declare Points = 0.; declare Real[Text] DisciplineRecords for _Score; declare Real[Text] DisciplinePoints for _Score; // Loop through disciplines foreach (Discipline => Record in DisciplineRecords) { declare DisciPoints = C_PointsPerDiscipline; for (Index, 0, 5) { // Factor for algorithm if (G_Disciplines[Discipline] == 1 || G_Disciplines[Discipline] == 2) { DisciPoints *= (Record/G_DisciplineRecords[Discipline]); } else { DisciPoints *= (G_DisciplineRecords[Discipline]/Record); } } DisciplinePoints[Discipline] = DisciPoints; Points += DisciPoints; } _Score.Points = MathLib::NearestInteger(Points); } } Void UpdateAllPlayersScores() { foreach (Score in Scores) { UpdatePlayerScore(Score); } } // Queries all disciplines scores tab to be updated Void UpdateAllDisciplinesScores() { foreach (Discipline => Type in G_Disciplines) { G_DisciplineScoresUpdated[Discipline] = True; } } // Evaluate try and announce result Void EvaluateTry(CSmPlayer _Player, Real _Value, Boolean _TimeValue) { if (_Player.Score != Null && _Value > 0.) { declare Real Factor; if (_TimeValue) { Factor = -1.; } else { Factor = 1.; } declare Text Discipline for _Player; declare Real[Text] DisciplineRecords for _Player.Score; if (!DisciplineRecords.existskey(Discipline) || _Value * Factor > DisciplineRecords[Discipline] * Factor) { // New record! DisciplineRecords[Discipline] = _Value; if (!G_DisciplineRecords.existskey(Discipline) || _Value * Factor > G_DisciplineRecords[Discipline] * Factor) { G_DisciplineRecords[Discipline] = _Value; } // Announce record declare UI <=> UIManager.GetUI(_Player); if (UI != Null) { UI.SendNotice( """New Record: {{{_Value^GetUnitForDiscipline(Discipline)}}}!""", CUIConfig::ENoticeLevel::MatchInfo, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Record, 1); } // Update scores UpdateAllPlayersScores(); G_DisciplineScoresUpdated[Discipline] = True; } else { // No new record :( declare UI <=> UIManager.GetUI(_Player); if (UI != Null) { UI.SendNotice( """Try: {{{_Value^GetUnitForDiscipline(Discipline)}}}""", CUIConfig::ENoticeLevel::PlayerInfo, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Notice, 0); } } // Send callback if (!_Player.IsFakePlayer) { SendCallbackPlayerDisciplineFinish(_Player.Login, Discipline, _Value); } } } // Returns utility layer manialink Text GetUtilityLayerManialink() { return """