Page 1 of 2

SplitScreen Stunt Mode Script V0.8

Posted: 07 Sep 2012, 15:18
by supersmo
This started out as a hobby project to bring stunt mode to split screen before there were any stunt title packs available.

As it turned out the code was a lot of help to others who modified it to bring stunt mode online.
Well done kapsubm!

(The first ever stunt server online was made by TGYoshi and was not related to this project.)

Anyhow, I'm a splitscreen player so this little project is focused on splitscreen play.
I'll keep posting updates I make to the script here in case someone is interested.

Changelog
v0.8
  • Better looking stunt UI made with manialink frames.
  • Added "dirty" flags to optimize ui redraw.
Changelog
V0.7
  • Fixed respawn penalty and added setting to turn it off. (Thanx for the report kapsubm.)
  • Speeded up score countdown. (Feedback from kapsubm)
V0.6
  • Really fixed score sorting at end of match this time. ;)
  • Some code cleanup
V0.5
  • Fixed end of match scoring and matchmodes.
  • You can now watch en entire intro or just skip it :)
  • A tiny bit of code cleanup
To do:
  • Add setting to make respawn penalty cause score to score at latest checkpoint
  • Implement all standard game modes into this script (rounds, time attack, stunts, platform)
  • If possible implement a ui at the beginning of race allowing users to change settings in a convenient way.
  • Clean up the code

How to start SplitScreen stunt mode:
1. Copy the code below. (Click on Code: "Select All", Press Ctrl + C)
2. Start a normal splitscreen race with the maps you want to play.
3. Press F12.
4. Paste the code into the "Script" frame with Ctrl + V.
5. Adjust the settings in the script according to your liking. (Lines starting with: #Settings)
6. Press "Save and Test".

(If you press F12 while you're playing stunt mode the script editor might display a loaded script other than the "Stunt Mode script". To make the editor show the "Stunt Mode script" press INCLUDES until you see a list of loaded scripts and then select Main. You can then adjust the settings and press "Save and Test".)

Code: Select all

// *********************** ** ** *   *
// *    SplitScreen Stunt competition
// ******************* ** ** *   *

#RequireContext CTmMode

#Include "TextLib" as TextLib
#Include "MathLib" as MathLib
#Include "Libs/Nadeo/Mode.Script.txt"
#Include "Libs/Nadeo/TrackMania/TM.Script.txt" as TM

// ******** StuntUi Settings *********
#Setting Splitscreen				True	//True if splitscreen; False if not splitscreen
#Setting SplitDirectionHorizontal	True	//True if splitscreen direction is horizontal; False if splitscreen direction is vertical (Only matters for 2 players)

// ******** Match Mode Settings *********
#Setting MatchMode 				0    	//0 = Match mode off; 1 = Points mode; 2 = Nr of rounds mode; 3 = Map playlist mode
#Setting PointsLimit 			30      //Nr of Points needed to win match.  (Only used when MatchMode is set to 1)
#Setting NrOfRoundsLimit 		5       //Nr of rounds to play before match ends. (Only used when MatchMode is set to 2)
#Setting CanMatchEndInTie 		False   //True = The match can end in a tie; False = Extra round(s) are played if winning score is tied at end of match

//********* GameMode Settings ********
#Setting GameMode 				2		// 0 = Rounds (Not working) 1 = TimeAttack (Not working see separate script); 2 = StuntMode; 3 = Platform(Not working)

// ******** TimeAttack/Stunt Settings **********
#Setting TimeLimitSeconds 		180     //Time limit for time attack/stunt round in seconds
#Setting AlwaysRespawnToStart 	False	//True = Respawn to start; False = Respawn to latest checkpoint, but respawn twice within 1 sec to respawn to start.
#Setting RespawnPenalty			-1		//0 = No penalty; -1 = default penalty(50 points); -2 = score reset to same as last Checkpoint (Not implemented yet)

// ******** Various Settings ******
#Setting RandomMapOrder 		False   //True = Random Map Order; False = Map Order of PlayList
#Setting ShowGoldTime 			False   //True = Show the map's gold time in upper left corner; False = Don't show gold time
#Setting ForceStuntFigureText 	False   //True = Show StuntFigureText regardless of game type; False = Show StuntFigureText depending on game type.

//***** Constants ******
#Const RoundPoints [10,6,4,3,2,1]
#Const GAME_MODE_ROUNDS      	0
#Const GAME_MODE_TIME_ATTACK	1
#Const GAME_MODE_STUNTS         2
#Const GAME_MODE_PLATFORM      	3

#Const MATCH_MODE_OFF           0   //In this mode there is no match, just plain old time attack.
#Const MATCH_MODE_POINTS        1   //In this mode the match ends when a player reaches the point limit.
#Const MATCH_MODE_NR_OF_ROUNDS  2   //In this mode the match ends after a certain number of rounds have been played.
#Const MATCH_MODE_PLAYLIST      3   //In this mode the match ends after all maps in playlist have been played.

#Const RESPAWN_PENALTY_NONE		 0
#Const RESPAWN_PENALTY_DEFAULT 	-1
#Const RESPAWN_PENALTY_LAST_CHECKPOINT -2  

#Const DOUBLE_CLICK_THRESHOLD   1000 //
#Const STUNT_TEXT_TIMEOUT       2000
#Const STUNT_MODE_COUNT_DOWN    60000 // Not configurable/working in ManiaPlanet 1.2e beta so setting it as a constant


//StuntModeStates
#Const STUNTS_ACTIVE	0
#Const STUNT_TIME_UP	1
#Const FINISHED_RACE	2

//#Const STUNTS_ACTIVE 	 0    	//Makes UI display CurRace.StuntsScore
//#Const COUNT_DOWN_SCORE -1      //Makes UI display CurRace.StuntsScore - countdown penalty
//#Const SKIP_REDRAW 		-2      //Used to optimize drawing of ui somewhat
//#Const FINISHED_RACE 	-3      //Makes UI display CurRace.StuntsScore at end of race
//StuntModeStates > 0 = time when to redraw regular score. (Last time a stunt was performed + STUNT_TEXT_TIMEOUT

declare Integer[Ident] RoundArrivalOrder;
declare Integer[] MapOrder;
declare Boolean IsSkipThisRound;
declare Text[CTmModeEvent::EStuntFigure] StuntFigureDictionary;
declare CTmResult::ETmRaceResultCriteria ResultCriteria;
declare Boolean ShowStuntFigureText;


Void InitializeGlobalVars() {
	switch(GameMode) {
		case GAME_MODE_ROUNDS: {
			ShowStuntFigureText = ForceStuntFigureText;
			ResultCriteria = CTmResult::ETmRaceResultCriteria::Time;
		}
		case GAME_MODE_TIME_ATTACK: {
			ShowStuntFigureText = ForceStuntFigureText;
			ResultCriteria = CTmResult::ETmRaceResultCriteria::Time;
		}
		case GAME_MODE_STUNTS: {
			ShowStuntFigureText = True;
			//ShowStuntUI = True;
			ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
		}
		case GAME_MODE_PLATFORM: {
			ShowStuntFigureText = ForceStuntFigureText;
			ResultCriteria = CTmResult::ETmRaceResultCriteria::NbRespawns;
		}
	}
}


// === Scores ===
Boolean Event_IsPenalty(CTmModeEvent _Event) {
	return _Event.StuntFigure == CTmModeEvent::EStuntFigure::RespawnPenalty
			|| _Event.StuntFigure == CTmModeEvent::EStuntFigure::TimePenalty;
}

Integer GetResultValue(CTmResult _Result, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	declare Integer ResultValue;
	switch (_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time:
			ResultValue = _Result.Time;
		case CTmResult::ETmRaceResultCriteria::Stunts:
			ResultValue = _Result.StuntsScore;
		case CTmResult::ETmRaceResultCriteria::NbRespawns:
			ResultValue = _Result.NbRespawns;
		default:
			assert(False); //Shouldn't be here! FIx it!
	}
	return ResultValue;
}

Integer GetResultValue(CTmModeEvent _Event,  CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	declare Integer ResultValue;
	declare Integer CheckpointCount for _Event.Player;
	switch (_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time:
			ResultValue = _Event.RaceTime;
		case CTmResult::ETmRaceResultCriteria::Stunts:
		
			if (Event_IsPenalty(_Event))
				switch (RespawnPenalty) {
					case RESPAWN_PENALTY_DEFAULT: {
						if (CheckpointCount == 0) {
							ResultValue = 0;
						} else {
							declare Integer StuntsScore for _Event.Player;
							if (_Event.Points > StuntsScore) {
								ResultValue = -StuntsScore;
							} else {
								ResultValue = -_Event.Points;
							}
						}
					}
					case RESPAWN_PENALTY_NONE:
						ResultValue = 0;
					case RESPAWN_PENALTY_LAST_CHECKPOINT:
						assert(False); //Not implemented yet
					default: {
						if (CheckpointCount == 0) {
							ResultValue = 0;
							log("Checkpoint count was 0 -> No penalty");
						} else {
							declare Integer StuntsScore for _Event.Player;
							if (MathLib::Abs(RespawnPenalty) > StuntsScore) {
								ResultValue = -StuntsScore;
								log("Penalty larger than score. Penalty = score.");
							} else {
								ResultValue = -MathLib::Abs(RespawnPenalty);
								log("There is enough points draw the full penalty");
							}
						}
					}
				}
			else {
				ResultValue = _Event.Points;
			}
		case CTmResult::ETmRaceResultCriteria::NbRespawns:
			ResultValue = _Event.NbRespawns;
		default:
			assert(False); //Shouldn't be here! FIx it!
	}
	//log("Event StuntScore" ^ResultValue);
	return ResultValue;
}


Integer ResultValue_Compare(Integer _ResultValue1, Integer _ResultValue2, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	declare Integer ResultValueDiff;
	switch(_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time:
			ResultValueDiff = _ResultValue2 - _ResultValue1;
		case CTmResult::ETmRaceResultCriteria::Stunts:
			ResultValueDiff = _ResultValue1 - _ResultValue2;
		case CTmResult::ETmRaceResultCriteria::NbRespawns:
			ResultValueDiff = _ResultValue2 - _ResultValue1;
		default:
			assert(False); //Shouldn't reach this point. 
	}
	return ResultValueDiff;
}

Boolean IsResultBetter(Integer _ResultValue1, Integer _ResultValue2, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	return ResultValue_Compare(_ResultValue1, _ResultValue2, _ResultCriteria) > 0;
}


Boolean IsResultBetter(CTmResult _Result1, CTmResult _Result2, CTmResult::ETmRaceResultCriteria _ResultCriteria)
{
	declare Boolean IsBetter;
	if ( _Result1 == Null ) {
		IsBetter =  False;
		
	}
	else if ( _Result2 == Null) {
		IsBetter =  True;
	}
	else {
		declare Comp = _Result1.Compare(_Result2, _ResultCriteria);
		IsBetter =  Comp > 0;
	}
	return IsBetter;
}

Integer[Text] SortResultValueArray(Integer[Text] _ResultValueArray, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	declare Integer[Text] SortedArray;
	switch(_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time:
			SortedArray = _ResultValueArray.sort();
		case CTmResult::ETmRaceResultCriteria::Stunts: {
			foreach (Name => ResultValue in _ResultValueArray) {
				SortedArray[Name] = -ResultValue;
			}
			SortedArray = SortedArray.sort();
			foreach (Name => ResultValue in SortedArray){
				SortedArray[Name] = MathLib::Abs(ResultValue);
			}
		}
		case CTmResult::ETmRaceResultCriteria::NbRespawns:
			SortedArray = _ResultValueArray.sort();
		default:
			assert(False); //Shouldn't reach this point.
	}
	return SortedArray;
}

Integer[Ident] SortResultValueArray(Integer[Ident] _RoundArrivalArray, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	declare Integer[Ident] SortedArray;
	switch(_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time:
			SortedArray = _RoundArrivalArray.sort();
		case CTmResult::ETmRaceResultCriteria::Stunts: {
			foreach (Id => ResultValue in _RoundArrivalArray) {
				SortedArray[Id] = -ResultValue;
			}
			SortedArray = SortedArray.sort();
			foreach (Id => ResultValue in SortedArray){
				SortedArray[Id] = MathLib::Abs(ResultValue);
			}
		}
		case CTmResult::ETmRaceResultCriteria::NbRespawns:
			SortedArray = _RoundArrivalArray.sort();
		default:
			assert(False); //Shouldn't reach this point.
	}
	return SortedArray;
}

//Wasn't allowed to override Scores_Sort :(
Void SortScores(CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	switch(_ResultCriteria) {
		
		case CTmResult::ETmRaceResultCriteria::Time:
		Scores_Sort(::ETmScoreSortOrder::BestRace_Time);
		case CTmResult::ETmRaceResultCriteria::Stunts:
		Scores_Sort(::ETmScoreSortOrder::BestRace_Stunts);
	case CTmResult::ETmRaceResultCriteria::NbRespawns:
		Scores_Sort(::ETmScoreSortOrder::BestRace_NbRespawns);
		default:
			assert(False); //Shouldn't reach this point.
	}
}



Void ClearScoreTimes() {
	foreach (Score in Scores) {
		Score.BestRace.Time = -1;
		Score.BestLap.Time = -1;
		Score.PrevRace.Time = -1;
		Score.TempResult.Time = -1;
		
		Score.BestRace.StuntsScore = 0;
		Score.BestLap.StuntsScore = 0;
		Score.PrevRace.StuntsScore = 0;
		Score.TempResult.StuntsScore = 0;
		
		Score.BestRace.NbRespawns = -1;
		Score.BestLap.NbRespawns = -1;
		Score.PrevRace.NbRespawns = -1;
		Score.TempResult.NbRespawns = -1;
	}
}

Void ClearStuntUI(CTmPlayer _Player) {
	declare Text 	StuntFigureText for _Player;
	declare Integer StuntPoints for _Player;
	declare Integer StuntsScore for _Player;
	declare Integer StuntsScoreAtTimeUp for _Player;
	declare Boolean IsStuntUIDirty for _Player;
	declare Integer StuntFigureTextTimer for _Player;
	
	StuntFigureText = "";
	StuntPoints = 0;
	StuntsScore = 0;
	StuntsScoreAtTimeUp = 0;
	StuntFigureTextTimer = 0;
	IsStuntUIDirty = True;
}

Boolean IsPointsTied() {
	if (Scores.count > 1)
		return Scores[0].Points == Scores[1].Points;

	return False;
}

Integer CalcStuntScoreAtCountDown(CTmPlayer _Player, Integer _Time) {
	declare Integer StuntsScoreAtTimeUp for _Player;
	declare StuntScore = StuntsScoreAtTimeUp;
	if (_Player.RaceStartTime != 0 && _Time > STUNT_MODE_COUNT_DOWN) {
		StuntScore = StuntScore - (_Time - STUNT_MODE_COUNT_DOWN) / 200;
		if (StuntScore < 0)
			StuntScore = 0;
	}
	return StuntScore;
}


Boolean IsEndOfMatchReached(Integer _MatchRoundNr) {
	if (MatchMode == MATCH_MODE_POINTS && Scores.count > 0 && Scores[0].Points >= PointsLimit)
		return True;
	else if (MatchMode == MATCH_MODE_NR_OF_ROUNDS && _MatchRoundNr >= NrOfRoundsLimit)
		return True;
	else if (MatchMode == MATCH_MODE_PLAYLIST && _MatchRoundNr >= MapList.count)
		return True;
	return False;
}

Void UnSpawn(CTmPlayer _Player) {
	TM::EndRaceSequence_Remove(_Player);
	_Player.RaceStartTime = 0;
}

Integer[] CreateMapOrder(Boolean _shouldRandomize) {
	declare Integer[] newMapOrder;
	declare Integer maxMapNr = MapList.count-1;
	
	for(i,0,maxMapNr) {
		newMapOrder.add(i);
	}
	
	if (_shouldRandomize) {
		declare Integer tmp;
		declare Integer rand;
		
		for (i, 0, maxMapNr) {
			rand = MathLib::Rand(0, maxMapNr);
			tmp = newMapOrder[i];
			newMapOrder[i] = newMapOrder[rand];
			newMapOrder[rand] = tmp;
		}
	}
	return newMapOrder;
}

Text GetRaceResultText(Integer _Result, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
//log("GetRaceResultText called");
	declare Text RaceResultText;
	switch (_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time: {
			RaceResultText ^= TextLib::TimeToText(_Result, True);
		}
		case CTmResult::ETmRaceResultCriteria::Stunts: {
			RaceResultText ^= _Result ^" Points";
		}
		default:
			assert(False);  //Should not be here
	}
	return RaceResultText;
}

Text GetRaceResultText(CTmResult _Result, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
	declare Text RaceResultText;
	RaceResultText = GetRaceResultText(GetResultValue(_Result, _ResultCriteria), _ResultCriteria);
	
	return RaceResultText;
}





Text GetManiaLinkPage(Text _text, Integer _length, Integer _height) {
return """<frame posn="0 60">
			<quad  posn="0 3 -1"   sizen="{{{_length}}} {{{_height}}}"   halign="center"   style="Bgs1InRace" substyle="BgTitle3"  />
			<label posn="0 0"                  halign="center" scale="2" text="{{{_text}}}" />
			</frame>""";
}

Text GetManiaLinkPage(Text _text) {
	return GetManiaLinkPage(_text, 130, 14);
}

Void InitStuntFigureDictionary() {
	StuntFigureDictionary.clear();
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::None] =                "None???";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::StraightJump] =          "Jump";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Flip] =                "Flip";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::BackFlip] =             "Back Flip";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Spin] =                "Spin";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Aerial] =             "Aerial";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::AlleyOop] =             "Alley Ooop";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Roll] =                "Roll";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Corkscrew] =             "Corkscrew";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::SpinOff] =             "Spin Off";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Rodeo] =                "Rodeo";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::FlipFlap] =             "Flip Flap";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Twister] =             "Twiseter";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::FreeStyle] =             "Free Style";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::SpinningMix] =          "Spinning Mix";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::FlippingChaos] =          "Flipping Chaos";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::RollingMadness] =       "Rolling Madness";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckNone] =             "Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckStraightJump] =       "Jump Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFlip] =             "Flip Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckBackFlip] =          "Back Flip Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckSpin] =             "Spin Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckAerial] =          "Aerial Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckAlleyOop] =          "Alley Ooop Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckRoll] =             "Roll Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckCorkscrew] =       "Corkscrew Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckSpinOff] =          "Spin Off Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckRodeo] =          "Rodeo Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFlipFlap] =          "Flip Flap Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckTwister] =          "Twister Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFreeStyle] =       "Free Style Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckSpinningMix] =       "Spinning Mix Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFlippingChaos] =    "Flipping Chaos Wreck";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckRollingMadness] =   "Rolling Madness Wreck";
///
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::TimePenalty] =          "Time Penalty";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Reset] =                "Reset";
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::RespawnPenalty] =       "Respawn Penalty";
///
	StuntFigureDictionary[CTmModeEvent::EStuntFigure::Grind] =                "Grind";
}

Void LogEvent(CTmModeEvent _Event) {
	log("Type: "^_Event.Type ^ " RaceTime:"^ _Event.RaceTime ^" StuntFigure: "^_Event.StuntFigure^ " Angle: "^_Event.Angle^" Points: "^_Event.Points^" Combo: "^_Event.Combo^" IsStraight: "^_Event.IsStraight^" IsReverse: "^_Event.IsReverse^" IsMasterJump: "^_Event.IsMasterJump^" Factor: "^_Event.Factor^" StuntsScore: "^_Event.StuntsScore^" Speed: "^ _Event.Speed^ " Distance: "^_Event.Distance^ " Damages: "^_Event.Damages);
}

Text GetStuntText(CTmModeEvent _Event) {
	declare Integer CheckpointCount for _Event.Player;
	declare Boolean IsPenalty = Event_IsPenalty(_Event);
	declare Text StuntText = "";
	
//	log("StuntFigure: "^_Event.StuntFigure^ " Angle: "^_Event.Angle^" Points: "^_Event.Points^" Combo: "^_Event.Combo^" IsStraight: "^_Event.IsStraight^" IsReverse: "^_Event.IsReverse^" IsMasterJump: "^_Event.IsMasterJump^" Factor: "^_Event.Factor^" StuntsScore: "^_Event.StuntsScore^" Speed: "^ _Event.Speed^ " Distance: "^_Event.Distance^ " Damages: "^_Event.Damages);
	
	/*
	(Chained)(Master) (Straight) (Basic) xxx (Angle)(!!!)
		+ 34
		50 Points
	*/
	
	if (_Event.Combo > 0) {
		if (_Event.Combo > 1)
			StuntText ^= """{{{_Event.Combo}}}X""";
		StuntText ^= " Chained";
	}
	
	if (_Event.IsMasterJump) 
		StuntText ^= " Master";
	if (_Event.IsStraight)
		StuntText ^= " Straight";
	if (_Event.Angle == 0 && !IsPenalty) {
		StuntText ^= " Basic";
	}

	if (!IsPenalty || (IsPenalty && (RespawnPenalty != RESPAWN_PENALTY_NONE) && CheckpointCount > 0)) {
		StuntText ^=""" {{{StuntFigureDictionary[_Event.StuntFigure]}}}""";
	}
	
	if (_Event.Angle != 0) {
		StuntText ^= """ {{{_Event.Angle}}}""";
		for (i, 1, _Event.Angle/180)
			StuntText ^= "!";
	}
	return StuntText;
}


Void InitStuntUIFields() {
	
	//Create stuntui for each player
	foreach (Player, Players) {
		declare Integer centerX for Player = 0;
		declare Integer centerY for Player = 0;
	}
	
	declare Integer InvScaleX for UIManager.UIAll;
	declare Integer InvScaleY for UIManager.UIAll;
	InvScaleX = 1;
	InvScaleY = 1;
	
		//log ("Players.count"^Players.count);	
	if (Splitscreen) {	
		//set Scale

		if (Players.count == 2) {
			if (SplitDirectionHorizontal) {
				InvScaleX = 1;
				InvScaleY = 2;
			} else {
				InvScaleX = 2;
				InvScaleY = 1;
			}
		} else if (Players.count == 3 || Players.count == 4) {
			InvScaleX = 2;
			InvScaleY = 2;
		}
		
		// set center for each player (pos 80 -45) = lower right quadrant
		for (playerIndex, 0, Players.count -1) {
			declare Integer centerX for Players[playerIndex];
			declare Integer centerY for Players[playerIndex];
			
			if (Players.count == 2) {
				if (SplitDirectionHorizontal) {
					centerX = 0;
					if (playerIndex == 0) 	//Player1
						centerY = 45;
					else 					//Player2
						centerY = -45;
				} else { 					//Vertical split
					centerY = 0;
					if (playerIndex == 0) 	//Player1
						centerX = -80;
					else 					//Player2 
						centerX = 80;
				}
			} else {	//3 or 4 players
				switch (playerIndex) {
					case 0: {
						centerX = -80;
						centerY = 45;
					}
					case 1: {
						centerX = -80;
						centerY = -45;
					}
					case 2: {
						centerX = 80;
						centerY = 45;
					}
					case 3: {
						centerX = 80;
						centerY = -45;
					}
				}
			}
		}
	}
}

Void UpdateStuntScoreUI(CTmPlayer _Player) {
	declare Text StuntFigureText for _Player;	
	declare Integer StuntPoints for _Player;
	declare Integer StuntsScore for _Player;
	declare CUILayer StuntUILayer for _Player;
	declare Integer centerX for _Player;
	declare Integer centerY for _Player;
	declare Integer InvScaleX for UIManager.UIAll;
	declare Integer InvScaleY for UIManager.UIAll;
	
	declare Text StuntPointsText = "";
	if (StuntPoints > 0) {
		StuntPointsText = "+" ^StuntPoints;
	} else if (StuntPoints < 0) {
		StuntPointsText ^= StuntPoints;
	}
	

	
//	log("centerX"^centerX);
//	log("centerY"^centerY);
//	log("InvScaleX"^InvScaleX);
//	log("InvScaleY"^InvScaleY);
	
	StuntUILayer.ManialinkPage = """<frame posn="{{{centerX}}} {{{centerY}}}" >
			<quad  posn="{{{160/InvScaleX}}} {{{39/InvScaleY}}} -1" valign="center" halign="right"  sizen="23 10"    style="Bgs1InRace" substyle="BgSlider"  />
						<quad  posn="{{{158/InvScaleX}}} {{{39/InvScaleY}}} 0" valign="center" halign="right"  sizen="6 6"    style="BgRaceScore2" substyle="Points"  />
			<label posn="{{{80/InvScaleX}}} {{{70/InvScaleY}}}"  style="TextRaceMessage" valign="center"          halign="right"     scale="1.5" text="$ff0{{{StuntFigureText}}}" />
			<label posn="{{{80/InvScaleX}}} {{{70/InvScaleY-8}}}"  style="TextRaceMessage" valign="center"          halign="right"     scale="1.5" text="$ff0{{{StuntPointsText}}}" />
			<label posn="{{{160/InvScaleX-9}}} {{{39/InvScaleY}}}" style="TextValueMedium"  valign="center"          halign="right"     scale="1" text="{{{StuntsScore}}}" />
			</frame>""";
	//UpdateStuntScoreUI(_Player, StuntFigureText, StuntPoints, StuntsScore);
}

Void logUI(CUIConfig _UI, Text _text) {
	log(_text ^" UIStatus: " ^_UI.UIStatus);
	log("Number of layers: "^_UI.UILayers.count);
	//log("IsHidden: "^_UI.OverlayHideNotices ^ _UI.OverlayHideMapInfo^_UI.OverlayHideOpponentsInfo^_UI.OverlayHideChat^_UI.OverlayHideCheckPointList^_UI.OverlayHideRoundScores^_UI.OverlayHideAll^_UI.OverlayHideScoresOnAltMenu^_UI.NoticesFilter_HidePlayerInfo^_UI.NoticesFilter_HidePlayerWarning^_UI.NoticesFilter_HidePlayerInfoIfNotMe^_UI.NoticesFilter_HidePlayerWarningIfNotMe^_UI.NoticesFilter_HideMapInfo^_UI.NoticesFilter_HideMapWarning^_UI.NoticesFilter_HideMatchInfo^_UI.NoticesFilter_HideMatchWarning);
	log("ScoreTableVisibility: "^_UI.ScoreTableVisibility^" SmallScoreTableVisibility: "^_UI.SmallScoreTableVisibility^"    AlliesLabelsVisibility: "^_UI.AlliesLabelsVisibility^" EnemiesLabelsVisibility: "^_UI.EnemiesLabelsVisibility^" EnemiesLabelsShowGauges: "^_UI.EnemiesLabelsShowGauges);
	log("ManiaLinkPage :"^ _UI.ManialinkPage);
}

//Best times shown in upper left corner 
Text GetFrameMapResults(CTmResult::ETmRaceResultCriteria _ResultCriteria)
{
	declare Integer[Text] ResultValues;
	if (ShowGoldTime && _ResultCriteria == CTmResult::ETmRaceResultCriteria::Time) {
		ResultValues["Gold"] = Map.TMObjective_GoldTime;
	}
	foreach (Score in Scores) {
		switch (_ResultCriteria) {
			case CTmResult::ETmRaceResultCriteria::Time: {
				if (Score.BestRace.Time != -1)
					ResultValues[Score.User.Name] = Score.BestRace.Time;
			}
			case CTmResult::ETmRaceResultCriteria::Stunts: {
				if (Score.BestRace.StuntsScore != -1)
					ResultValues[Score.User.Name] = Score.BestRace.StuntsScore;
			}
			default:
				assert(False); //Should not reach this point
		}
	}
	
	ResultValues = SortResultValueArray(ResultValues, _ResultCriteria);
	
	
	declare Text Legend;
	switch (_ResultCriteria) {
		case CTmResult::ETmRaceResultCriteria::Time:
			Legend = _("$oBest times:");
		case CTmResult::ETmRaceResultCriteria::Stunts:
			Legend = _("$oBest scores:");
		default:
			assert(False); //Shouldn't reach this point
	}
	
	declare Frame = """   <frame posn="-160 86">
	<quad posn="0 4 -1"    style="Bgs1" substyle="BgList" sizen="46 {{{3*(ResultValues.count +1) + 2}}}" />
						<label posn="1 3"    style="TextRaceStaticSmall" sizen="50 2" halign="left" text="{{{Legend}}}"  />""";
	declare Line = 0;
	foreach (Name => Result in ResultValues) {
		Frame ^= """<label posn="2 {{{-3*Line}}}"    style="TextRaceStaticSmall" sizen="25 3" halign="left" text="$ff0{{{Name}}}"  />
						<label posn="30 {{{-3*Line}}}"    style="TextPlayerCardScore" sizen="25 3" halign="left" text="$ff0{{{GetRaceResultText(Result, _ResultCriteria)}}}"  />""";
		Line += 1;
	}
	
	Frame ^=  """</frame>""" ;
	
	return Frame;
}

declare CUILayer UILayerScores;
declare CUILayer UILayerInfo;


//  ============ Intro Sequence:
Void PlayIntroSequence(Text _InfoText){
	sleep(0);   // flush loading lag.
	UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
	UIManager.UIAll.UILayers.clear();
	UIManager.UIAll.UILayers.add(UILayerScores);
	
	UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::Intro;
	
	//sleep(4000);   // HACK; should be "wait(sequences over)"
	wait(UIManager.UIAll.UISequenceIsCompleted);
	
	if (_InfoText == "")
		UILayerInfo.ManialinkPage = "";
	else {
		UILayerInfo.ManialinkPage = GetManiaLinkPage(_InfoText);
	}
	UIManager.UIAll.UILayers.add(UILayerInfo);
	
	UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
	foreach(Player, Players) {
		declare UI <=> UIManager.UI[Player];
		UI.BigMessage = Player.User.Name;
		UI.BigMessageAvatarLogin = Player.User.Login;
		UI.BigMessageAvatarVariant = CUIConfig::EAvatarVariant::Default;
		Player.RaceStartTime = Now + 100000;         // spawn the cars.
	}
	sleep(3000);
	
	//Remove UILayerInfo
	UILayerInfo.ManialinkPage = "";
	
	
	UIManager.UIAll.BigMessage = "";
	UIManager.UIAll.StatusMessage = "";
	foreach(Player, Players) {
		declare UI <=> UIManager.UI[Player];
		UI.BigMessage = "";
		UI.BigMessageAvatarLogin = "";
		UI.StatusMessage = "";
	}
}


// === Race rounds ==
Void DoTimeAttackRace()
{
	UiRounds = False;
	
	foreach (Player, Players) {
		declare CUILayer StuntUILayer for Player;
		if (Splitscreen) {
			UIManager.UIAll.UILayers.add(StuntUILayer);
		} else {
			UIManager.GetUI(Player).UILayers.add(StuntUILayer);
		}
	}
		
	
	if (GameMode == GAME_MODE_STUNTS) {  //Should be stuntmodeui= true
		UiStuntsMode = True;
		UiRaceChrono = CTmMode::ETmRaceChronoBehaviour::CountDown;
		UiDisplayStuntsNames = True; //Nadeo hasn't got it working yet. Extracted own stunt name  own StuntUI Layer.
		ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
	} else { //StuntMode Off
		ResultCriteria = CTmResult::ETmRaceResultCriteria::Time;
	}
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
	
	TM::Players_SpawnAll(Now + 2000);
	foreach(Score, Scores) {
		Score.PrevRaceDeltaPoints = 0;
	}
	sleep(500);   
	Synchro_DoBarrier();

	RoundArrivalOrder = Integer[Ident];
	declare RoundArrivalOrderDirty = True; 
	

	foreach (Player, Players) {
		declare Integer LastRespawnTime for Player;
		LastRespawnTime = Now;
		
		declare Integer CheckpointCount for Player;
		CheckpointCount = 0;
	}
	declare Integer MatchBestResult for Map = -1;
	
	
	foreach(Player, Players) {
		declare Integer StuntModeState for Player;
		
		ClearStuntUI(Player);
		StuntModeState = STUNTS_ACTIVE;
		
		Player.CurRace.StuntsScore = 0;
	}
	
	
	UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
	
	CutOffTimeLimit = Now + TimeLimitSeconds * 1000 +2500;
	
	//=== Loop until all players finished racing
	declare NbContestants = Players.count;
	while(  Now < CutOffTimeLimit ) {
		TM::RunGame();
		TM::EndRaceSequence_Update();
		
		if (MatchEndRequested)
			return;

		// Process events queue
		foreach(Event, PendingEvents) {   
			//LogEvent(Event);
			
			PassOn(Event);
//			LogEvent(Event);
		//log("StuntFigure: "^Event.StuntFigure^ " Angle: "^Event.Angle^" Points: "^Event.Points^" Combo: "^Event.Combo^" IsStraight: "^Event.IsStraight^" IsReverse: "^Event.IsReverse^" IsMasterJump: "^Event.IsMasterJump^" Factor: "^Event.Factor^" StuntsScore: "^Event.StuntsScore^" Speed: "^ Event.Speed^ " Distance: "^Event.Distance^ " Damages: "^Event.Damages);
			
			declare Player <=> Event.Player;
			declare Text StuntFigureText for Player;	
			declare Integer StuntFigureTextTimer for Player;
			declare Integer StuntPoints for Player;
			declare Integer StuntsScore for Player;
			declare Integer StuntsScoreAtTimeUp for Player;
			declare Boolean IsStuntUIDirty for Player;
			declare Integer StuntModeState for Player;
			declare Integer CheckpointCount for Player;
			declare Integer LastRespawnTime for Player;
			
			if (Event.Type == CTmModeEvent::EType::Stunt) {
				//Player.CurRace.StuntsScore += GetResultValue(Event, ResultCriteria);
				StuntFigureText = GetStuntText(Event);
				StuntPoints = GetResultValue(Event, ResultCriteria);
				StuntsScore += StuntPoints;
				StuntsScoreAtTimeUp = StuntsScore;
				
				IsStuntUIDirty = True;
				//StuntsScore[Player.Id] += Event.Points;
				
				//UpdateStuntScoreUI(Event.Player, GetStuntText(Event), GetResultValue(Event, ResultCriteria));         
				StuntFigureTextTimer = Now + STUNT_TEXT_TIMEOUT;
			}
			
			//if AlwaysRespawn to Start or respawn before first checkpoint or second respawn within 1 sec  -> make player respawn at beginning and reset time.
			else if (Event.Type == CTmModeEvent::EType::Respawn) {
				if (AlwaysRespawnToStart || CheckpointCount == 0 || Now - LastRespawnTime <= DOUBLE_CLICK_THRESHOLD) {
					UnSpawn(Player);
					
				}
				LastRespawnTime = Now;
			} else if (Event.Type == CTmModeEvent::EType::GiveUp) {
				if (MatchBestResult == -1) {
					IsSkipThisRound = True;
					UILayerInfo.ManialinkPage = GetManiaLinkPage("Skipping this round!");
					UIManager.UIAll.UILayers.add(UILayerInfo);
					sleep(1500);
					UIManager.UIAll.UILayers.clear();
				}
				
				TM::Players_UnspawnAll();
				CutOffTimeLimit = Now -1;
				
				
				break;
			} else if (Event.Type == CTmModeEvent::EType::StartLine) {
				CheckpointCount = 0;
				StuntModeState = STUNTS_ACTIVE; //Displays stuntui when respawned
				ClearStuntUI(Player);
			} else if (Event.Type == CTmModeEvent::EType::WayPoint) {
				CheckpointCount +=1;
				
				if (Event.IsEndRace) {
				
					//log("StuntsScore: "^Player.CurRace.StuntsScore);
					assert(Player.CurRace.Time == Event.RaceTime);
					StuntsScore = CalcStuntScoreAtCountDown(Player, Event.RaceTime);
					Player.CurRace.StuntsScore = StuntsScore;
					
					StuntFigureText = "";
					StuntPoints = 0;
					StuntFigureTextTimer = 0;
					IsStuntUIDirty = True;
					
					StuntModeState = FINISHED_RACE; //Stops the countdown in StuntUI and displays the final score.
					
					Player.Score.PrevRace = Player.CurRace;
					//log("End of race! StuntsScore: "^Player.Score.PrevRace.StuntsScore);
					
					declare Text ResultText = "$o$EA2" ^ GetRaceResultText(Player.CurRace, ResultCriteria);
					
					if (IsResultBetter(Player.CurRace, Player.Score.BestRace, ResultCriteria)) {
						//log("some kind of record");
						Player.Score.BestRace = Player.CurRace;
				
						SortScores(ResultCriteria);
				
						//log("Best Race Set");

						if (IsResultBetter(GetResultValue(Player.CurRace, ResultCriteria), MatchBestResult, ResultCriteria)) {
							//log("Match record!");
							MatchBestResult = GetResultValue(Player.CurRace, ResultCriteria);
							//log("Calling EndRaceSequence");
							TM::EndRaceSequence_Add(Player, TextLib::Compose(_("New match record!\n%1"), ResultText));
						} else {
							//log("Personal Record! Calling EndRaceSequence");
							TM::EndRaceSequence_Add(Player, TextLib::Compose(_("Personal record! %1"), ResultText));
						}
						RoundArrivalOrder[Player.Id] = GetResultValue(Event, ResultCriteria);
						RoundArrivalOrderDirty = True;
					} else {
//						log("No Record! Calling EndRaceSequence");
//                  log("CurRace" ^Player.CurRace.StuntsScore ^ " MatchBestResult:"^MatchBestResult.StuntsScore);
						if (GameMode != GAME_MODE_STUNTS)
							ResultText = "";
						TM::EndRaceSequence_Add(Player, ResultText);
					}
				}
			}
		}
		
		//Not fully optimized, could have a proper Dirty flag...
		foreach(Player, Players) {
			declare Integer StuntModeState for Player;
			declare Boolean IsStuntUIDirty for Player;
			declare Integer StuntFigureTextTimer for Player;
			declare Text StuntFigureText for Player;
			declare Integer StuntPoints for Player;
			declare Integer StuntsScore for Player;
			
			//assert (Player.Id!= prevPlayerId);
			
			if (StuntFigureTextTimer > 0 && Now > StuntFigureTextTimer) {
				StuntFigureTextTimer = 0;
				StuntFigureText = "";
				StuntPoints = 0;
				IsStuntUIDirty = True;
			}// else if (StuntModeState == FINISHED_RACE) {
				//UpdateStuntScoreUI(Player, "", 0);
			//}
			
			
			//Todo Optimize with stuntmode state so interface is updated on transition and when score changes.
			//Detect Stunt time up
			declare CurRaceTime = Now - Player.RaceStartTime;
			if (StuntModeState == STUNTS_ACTIVE && Player.RaceStartTime > 0 && CurRaceTime > STUNT_MODE_COUNT_DOWN) {
					//&& StuntModeState != FINISHED_RACE) {
					StuntModeState = STUNT_TIME_UP;
					StuntFigureText = "$f00Go to the Finish!";
					StuntFigureTextTimer = 0;
					StuntPoints = 0;
					StuntsScore = CalcStuntScoreAtCountDown(Player, CurRaceTime);
					IsStuntUIDirty = True;
				//UpdateStuntScoreUI(Player, "Go to the Finish!", 0, CalcStuntScoreAtCountDown(Player, CurRaceTime));
			}
			
			if (StuntModeState == STUNT_TIME_UP) {
				declare Integer PrevStuntsScore = StuntsScore;
				StuntsScore = CalcStuntScoreAtCountDown(Player, CurRaceTime);
				if (PrevStuntsScore != StuntsScore)
					IsStuntUIDirty = True;
			}
			//prevPlayerId = Player.Id;
			
			if (IsStuntUIDirty) {
				UpdateStuntScoreUI(Player);
				IsStuntUIDirty = False;
			}
		}

		// Refresh score table.      
		if (RoundArrivalOrderDirty) {
			RoundArrivalOrderDirty = False;
			RoundArrivalOrder = SortResultValueArray(RoundArrivalOrder, ResultCriteria);
						
			UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);      // refresh scores...
		}
		
		// Respawn unspawned players
		if (PlayersWaiting.count > 0) {
			TM::Players_SpawnWaiting(0);
		}
	}
	
	foreach (Player, Players) {
		declare Boolean IsStuntUIDirty for Player;
		ClearStuntUI(Player);
		UpdateStuntScoreUI(Player);
		IsStuntUIDirty = False;
	}

	
	TM::Players_UnspawnAll();
	
	Synchro_DoBarrier();

	// endround sequence
	sleep(1000);
}

Void PlayEndOfRoundSequence(Text _InfoText, Integer _MatchRoundNr) {
	UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound;
	UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
	
	
	sleep(2500);   //Showing race times
	
	if (MatchMode != MATCH_MODE_OFF) {   //Display match point sequence
		UiRounds = True;
		UiStuntsMode = False;  // In order to display rounds points properly
		
		if (_InfoText == "")
			UILayerInfo.ManialinkPage = "";
		else {
			UILayerInfo.ManialinkPage = GetManiaLinkPage(_InfoText);
		}
		UIManager.UIAll.UILayers.add(UILayerInfo);

		declare Index = 0;
		declare Text Table = """<scoretable type="time">""";
			
		declare Ident PrevPlayerId = NullId;
		declare ScoreInc = 0;
		foreach (PlayerId => Time in RoundArrivalOrder) {
			declare Player <=> Players[PlayerId];      // faut le PlayerInfo
			
			//Only update scorInc if current player != sameResult as previous player.
			declare Boolean IsSameTimeAsPrevPlayer = (PrevPlayerId != NullId && Time == RoundArrivalOrder[PrevPlayerId]);
			
			if (!IsSameTimeAsPrevPlayer) {
				if (Index < RoundPoints.count)
					ScoreInc = RoundPoints[Index];
				else
					ScoreInc = 1;
				
				if (Index == Players.count -1)      // last players gets no points.
					ScoreInc = 0;            
			}
			Index += 1;
			
			Table ^= """<score login="{{{Player.Login}}}" value="{{{Time}}}" inc="{{{ScoreInc}}}"/>""";      
			Player.Score.PrevRaceDeltaPoints = ScoreInc;
			PrevPlayerId = PlayerId;
		}
		Table ^= """</scoretable>""";
		UIManager.UIAll.SmallScoreTable = Table;

		sleep(2000);   //Showing DeltaPoints

		foreach(Score, Scores) {
			Score.Points += Score.PrevRaceDeltaPoints;
			Score.PrevRaceDeltaPoints = 0;
		}
		
		Scores_Sort(::ETmScoreSortOrder::TotalPoints);   
		if (IsPointsTied() && IsEndOfMatchReached(_MatchRoundNr))
			UILayerInfo.ManialinkPage = GetManiaLinkPage("Points are tied! Next round will be a tiebreaker!", 210, 14);
		
		sleep(2500); //Showing TotalPoints
		UILayerInfo.ManialinkPage = "";
	}
	
	UIManager.UIAll.SmallScoreTable = "";
	UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
	
	sleep(500);   
	Synchro_DoBarrier();
}


//==== EndOfMatchSequence ====
Void EndOfMatchSequence() {
	TM::Players_UnspawnAll();
	UiRounds = True;

	if (Scores.count > 0 && Scores[0].Points > 0 && !ServerShutdownRequested) {
		UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
		UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium;
		sleep(2500);
		
		declare Text WinnerNames = Scores[0].User.Name;
		declare TextBoxWidth = 130;
		
		for (i, 1, Scores.count -1) {
			if ( Scores[i].Points == Scores[0].Points) {
				WinnerNames ^= TextLib::MLEncode(" & ") ^Scores[i].User.Name;
				TextBoxWidth = 320;
			}
			else 
				break;
		}
		
		
		declare VictoryText = TextLib::Compose(_("$<%1$> wins the match!"), WinnerNames);
		UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
		UILayerInfo.ManialinkPage = GetManiaLinkPage(VictoryText, TextBoxWidth, 14);
		

		UIManager.UIAll.UILayers.clear();
		UIManager.UIAll.UILayers.add(UILayerScores);
		UIManager.UIAll.UILayers.add(UILayerInfo);

		sleep(3000);
		UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
		sleep(5000);
	}
	
	Synchro_DoBarrier();

	UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
	UIManager.UIAll.UILayers.clear();

	Scores_Clear();
	
	MatchEndRequested = False;      // taken into account.
}

// === Main ===
main() 
{
	//Init
	UIManager.ResetAll();
	log("restart...");
	
	InitializeGlobalVars();
	InitStuntUIFields();
	
	
	IndependantLaps = True;
	
	//Don't use ::ETMRespawnBehaviour::GiveUpBeforeFirstCheckPoint or ETMRespawnBehaviour::AlwaysGiveUp
	//because the "give up" event is needed to implement skip current map feature.
	RespawnBehaviour = ::ETMRespawnBehaviour::Normal;

	UILayerScores <=> UIManager.UILayerCreate();
	UILayerInfo <=> UIManager.UILayerCreate();
	
	foreach(Player, Players) {
		declare CUILayer StuntUILayer for Player;
		StuntUILayer <=> UIManager.UILayerCreate();
	}
	
	InitStuntFigureDictionary();

	MapOrder = CreateMapOrder(RandomMapOrder);
	declare MapOrderIndex = 0;
	
	//Since the script needs to be pasted instead of loaded there is already a loaded map.
	//Need to unload it to enforce map order.
	if (MapLoaded)
		UnloadMap();
	
	// loop until server interrupted
	while( !ServerShutdownRequested ) {
		declare MatchRoundNr = 1;
		declare IsTieBreakRound = False;
		
		declare IntroText = "";
		declare EndOfRoundText = "";
		
		//make sure new match start with first map in maplist in playlist mode. (Last match could've ended in tiebreak.)
		if (MatchMode == MATCH_MODE_PLAYLIST) {
			MapOrderIndex = 0;
		}
		
		// ============ Play the Time Attack races until the end of match condition is reached:
		while( !MatchEndRequested ) {
			IsSkipThisRound = False;
			
			//Display scoreTable before match in Rounds mode if a Match Mode is set
			if (MatchMode!= MATCH_MODE_OFF)
				UiRounds = True;
			UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
			UIManager.UIAll.UISequence = CUIConfig::EUISequence::Intro;

			NextMapIndex = MapOrder[MapOrderIndex];

			LoadMap();

			ClearScoreTimes();  //Don't call "Scores_Clear" because we want to preserve the match points
			
			TM::Players_UnspawnAll();
			
			
			//Set texts according to settings and state
			if (IsTieBreakRound) {
				IntroText = "Tiebreaker round!";
				EndOfRoundText = "Player with most points wins!";
			} else if (MatchMode == MATCH_MODE_POINTS) {
				if (MatchRoundNr == 1)
					IntroText = "New match!";
				else
					IntroText = "";
				EndOfRoundText = """{{{PointsLimit}}} points to win!""";
			} else if (MatchMode == MATCH_MODE_NR_OF_ROUNDS) {
				IntroText = """Round {{{MatchRoundNr}}} of {{{NrOfRoundsLimit}}}""";
				EndOfRoundText = """Round {{{MatchRoundNr}}} of {{{NrOfRoundsLimit}}}""";
			} else if (MatchMode == MATCH_MODE_PLAYLIST) {
				IntroText = """Round {{{MatchRoundNr}}} of {{{MapList.count}}}""";
				EndOfRoundText = """Round {{{MatchRoundNr}}} of {{{MapList.count}}}""";
			}
			
			PlayIntroSequence(IntroText);
			DoTimeAttackRace();
			if (!IsSkipThisRound)
				PlayEndOfRoundSequence(EndOfRoundText, MatchRoundNr);
			
			if (IsEndOfMatchReached(MatchRoundNr)) {
				if (!CanMatchEndInTie && IsPointsTied())
					IsTieBreakRound = True;
				else {
					IsTieBreakRound = False;
					break;       // score limit reached.
				}
			}

			if (!IsSkipThisRound || MatchMode == MATCH_MODE_PLAYLIST)
				MatchRoundNr += 1;
			
			MapOrderIndex += 1;
			if (MapOrderIndex >= MapList.count) {
				MapOrderIndex = 0;
			}

			UnloadMap();
		}
		
		// ============ End of Match Sequence:
		if (MatchMode != MATCH_MODE_OFF)
			EndOfMatchSequence();
		
		UnloadMap();
	}
	
	// ====== Cleanup
	log("Cleanup! (Is this bit of code ever called???)");
	UIManager.UILayerDestroy(UILayerScores);
	UIManager.UILayerDestroy(UILayerInfo);
}

Re: SplitScreen Stunt Mode Script V0.4

Posted: 08 Sep 2012, 07:57
by kapsubm
tested the script in Servermode, and it seems to work, had to cut out the backspace part for next map :)

anyway:
Two questions:
1. Respawn gives 50 + points ? - and on end points a re subtracted. couldnt find the part where that calculations are done.
2. couldnt change the big chars ...

oh yes, i am really a noob on maniascript :)

Re: SplitScreen Stunt Mode Script V0.4

Posted: 08 Sep 2012, 13:19
by supersmo
kapsubm wrote:tested the script in Servermode, and it seems to work, had to cut out the backspace part for next map :)

anyway:
Two questions:
1. Respawn gives 50 + points ? - and on end points a re subtracted. couldnt find the part where that calculations are done.
2. couldnt change the big chars ...

oh yes, i am really a noob on maniascript :)
Interesting :) Haven't tried it in server mode. I' m a splitscreen player so my main target is Split screen.
Once I'm satisfied with split screen I can take a look at server mode.

1. The "End of match" stuff is currently messed up. I know how to fix it and will hopefully get around to doing it on Monday on my way to work. (I have wife and kids and very limited time to work on my spare time projects.)

2. The big chars are on my todo list. Right now I'm using the "BigMessage" thing instead of a custom UILayer with manialink text. When I change that I can have full controll over positioning and text size. BigMessage was just a quick fix to get something displayed and the logic worked out.

Glad someone is using it. Like I said my main target is SplitScreen, but if it some of it works in servermode that's a bonus. :)

Re: SplitScreen Stunt Mode Script V0.4

Posted: 09 Sep 2012, 05:02
by kapsubm
is it possible for yout to send a couple of events via xmlrpc callbacks ?
(checkpoint score, player finish score, along with the playername)
and on endmatch a playerlist with scores ?

Re: SplitScreen Stunt Mode Script V0.4

Posted: 09 Sep 2012, 09:10
by supersmo
Since I've been focusing on SplitScreen I haven't studied how Tm2 deals with communication between clients and server. I'd have to look at some examples of how this is done in order to do what you ask.

(Correct me if I'm wrong but I'm guessing xmlrpc has to do with server/client stuff.)

When I've got the SplitScreen Stunt Mode Script working in a satisfactory manor I'll see if I can find some good examples of server scripts and see what I can do.

Re: SplitScreen Stunt Mode Script V0.5

Posted: 10 Sep 2012, 18:55
by supersmo
Updated SplitScreen Stunt Mode Script to V0.5

Changelog
V0.5
  • Fixed end of match scoring and matchmodes. (Haven't tested it thoroughly though the sorting of the scores could be messed up but the scores should produce the correct results.)
  • You can now watch en entire intro or just skip it :)
  • A tiny bit of code cleanup
Let me know of any bugs. (Besides making a better looking stunt ui that doesn't block the view so much).

Re: SplitScreen Stunt Mode Script V0.5

Posted: 10 Sep 2012, 21:09
by kapsubm
got xmlrpc allready working,

(temporary each stunt)
playercheckpoint, playerfinish. endmatch a sorted palerlist is open as xmlrpc - but is on work.

only two things are in my task atm:

.Scoretable on Endround.
.Skip map Produce an error.

will post the script For Multiplayer server when those 2 things are done.

Known Issues: Alltough the server runs as "Race" no LP are gained.

Re: SplitScreen Stunt Mode Script V0.5

Posted: 11 Sep 2012, 12:20
by kapsubm
the script so far (works perfectly in splitscreen, single and multiplayermode)
(issues: Scoretable on end of round)
the Difference to your script is, that u can respan, the time limit is taken in account,
in this time all players can do stunts as long as the time limit is not null
(scoring 1 minute due to nadeo restriction)
if we can sort out the table sort and maybe ladderranks, it should be ready for release.

Code: Select all

// *********************** ** ** *   *
// *    Multiplayer Stunt competition
// ******************* ** ** *   *

#RequireContext CTmMode

#Include "TextLib" as TextLib
#Include "MathLib" as MathLib
#Include "Libs/Nadeo/Mode.Script.txt"
#Include "Libs/Nadeo/TrackMania/TM.Script.txt" as TM

// ******** Match Mode Settings *********
#Setting MatchMode 0               //0 = Match mode off; 1 = Points mode; 2 = Nr of rounds mode; 3 = Map playlist mode
#Setting PointsLimit 30            //Nr of Points needed to win match.  (Only used when MatchMode is set to 1)
#Setting NrOfRoundsLimit 5            //Nr of rounds to play before match ends. (Only used when MatchMode is set to 2)
#Setting CanMatchEndInTie False     //True = The match can end in a tie; False = Extra round(s) are played if winning score is tied at end of match

//********* GameMode Settings ********
#Setting GameMode 2                  // 0 = Rounds (Not working) 1 = TimeAttack (Not working see separate script); 2 = StuntMode; 3 = Platform(Not working)

// ******** TimeAttack Settings **********
#Setting TimeLimitSeconds 300         //Time limit for round in seconds
#Setting AlwaysRespawnToStart False   //True = Respawn to start; False = Respawn to latest checkpoint, but respawn twice within 1 sec to respawn to start.

// ******** Various Settings ******
#Setting RandomMapOrder False         //True = Random Map Order; False = Map Order of PlayList
#Setting ShowGoldTime False           //True = Show the map's gold time in upper left corner; False = Don't show gold time
#Setting ForceStuntFigrueText False   //True = Show StuntFigureText regardless of game type; False = Show StuntFigureText depending on game type.

//***** Constants ******
#Const RoundPoints [10,6,4,3,2,1]
#Const GAME_MODE_ROUNDS         0
#Const GAME_MODE_TIME_ATTACK   1
#Const GAME_MODE_STUNTS         2
#Const GAME_MODE_PLATFORM      3

#Const MATCH_MODE_OFF             0   //In this mode there is no match, just plain old time attack.
#Const MATCH_MODE_POINTS          1   //In this mode the match ends when a player reaches the point limit.
#Const MATCH_MODE_NR_OF_ROUNDS  2   //In this mode the match ends after a certain number of rounds have been played.
#Const MATCH_MODE_PLAYLIST       3   //In this mode the match ends after all maps in playlist have been played.

#Const DOUBLE_CLICK_THRESHOLD    1000 //
#Const STUNT_TEXT_TIMEOUT       1000
#Const STUNT_MODE_COUNT_DOWN    60000 // Not configurable/working in ManiaPlanet 1.2e beta so setting it as a constant

//My StuntUIStates
#Const REGULAR_SCORE 0               //Makes UI display CurRace.StuntsScore
#Const COUNT_DOWN_SCORE -1            //Makes UI display CurRace.StuntsScore - countdown penalty
#Const SKIP_REDRAW -2               //Used to optimize drawing of ui somewhat
#Const HAS_FINISHED -3               //Makes UI display CurRace.StuntsScore at end of race
//StuntUIStates > 0 = time when to redraw regular score. (Last time a stunt was performed + STUNT_TEXT_TIMEOUT

//#Const SORT_ASCENDING = 0
//#Const SORT_DECENDING = 0

declare Integer[Ident] RoundArrivalOrder;
declare Integer[Ident] StuntUIState;
declare Integer[] MapOrder;
declare Boolean IsSkipThisRound;
declare Text[CTmModeEvent::EStuntFigure] StuntFigureDictionary;
declare CTmResult::ETmRaceResultCriteria ResultCriteria;
declare Boolean ShowStuntFigureText;


Void InitializeGlobalVars() {
   switch(GameMode) {
      case GAME_MODE_ROUNDS: {
         ShowStuntFigureText = ForceStuntFigrueText;
         ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
      }
      case GAME_MODE_TIME_ATTACK: {
         ShowStuntFigureText = ForceStuntFigrueText;
         ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
      }
      case GAME_MODE_STUNTS: {
         ShowStuntFigureText = True;
         //ShowStuntUI = True;
         ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
      }
      case GAME_MODE_PLATFORM: {
         ShowStuntFigureText = ForceStuntFigrueText;
         ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
      }
   }
}


// === Scores ===
Integer GetResultScore(CTmResult _Result, CTmResult::ETmRaceResultCriteria _ResultCriteria) {
   declare Integer Score;
   switch (_ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time:
         Score = _Result.StuntsScore;
      case CTmResult::ETmRaceResultCriteria::Stunts:
         Score = _Result.StuntsScore;
      case CTmResult::ETmRaceResultCriteria::NbRespawns:
         Score = _Result.StuntsScore;
      default:
         assert(False); //Shouldn't be here! FIx it!
   }
   return Score;
}


//Doesn't work with negative Results!
Boolean IsResultBetter(CTmResult _Result1, CTmResult _Result2, CTmResult::ETmRaceResultCriteria _ResultCriteria)
{
   if (_Result1 != Null && _Result2 != Null)
      log("IsResultBetter: " ^GetResultScore(_Result1,_ResultCriteria)^" "^GetResultScore(_Result2,_ResultCriteria));
      
   declare Boolean IsBetter;
   if ( _Result1 == Null ) {
      log("Result1 was null");
      IsBetter =  False;
      
   }
   else if ( _Result2 == Null) {
      log("Result2 was null");
      IsBetter =  True;
   }
   else {
      declare Comp = _Result1.Compare(_Result2, _ResultCriteria);
      IsBetter =  Comp > 0;
   }
   return IsBetter;
}

Integer Result_Compare(Integer _Result1, Integer _Result2, CTmResult::ETmRaceResultCriteria ResultCriteria) {
   declare Integer Result;
   switch(ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time:
         Result = _Result2 - _Result1;
      case CTmResult::ETmRaceResultCriteria::Stunts:
         Result = _Result1 - _Result2;
      case CTmResult::ETmRaceResultCriteria::NbRespawns:
         Result = _Result2 - _Result1;
      default:
         assert(False); //Shouldn't reach this point.   
   }
   return Result;
}

Boolean IsResultBetter(Integer _Result1, Integer _Result2, CTmResult::ETmRaceResultCriteria ResultCriteria) {
   return Result_Compare(_Result1, _Result2, ResultCriteria) > 0;
}


Integer[Text] SortResultArray(Integer[Text] _ResultArray, CTmResult::ETmRaceResultCriteria ResultCriteria) {
   declare Integer[Text] SortedArray;
   switch(ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time:
         SortedArray = _ResultArray.sort();
      case CTmResult::ETmRaceResultCriteria::Stunts: {
         foreach (Name => Score in _ResultArray) {
            SortedArray[Name] = -Score;
         }
         SortedArray = SortedArray.sort();
         foreach (Name => Score in SortedArray){
            SortedArray[Name] = MathLib::Abs(Score);
         }
      }
      case CTmResult::ETmRaceResultCriteria::NbRespawns:
         SortedArray = _ResultArray.sort();
      default:
         assert(False); //Shouldn't reach this point.
   }
   return SortedArray;
}

Integer[Ident] SortRoundArrivalOrder(Integer[Ident] _RoundArrivalArray, CTmResult::ETmRaceResultCriteria ResultCriteria) {
   declare Integer[Ident] SortedArray;
   switch(ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time:
         SortedArray = _RoundArrivalArray.sort();
      case CTmResult::ETmRaceResultCriteria::Stunts: {
         foreach (Id => Score in _RoundArrivalArray) {
            SortedArray[Id] = -Score;
         }
         SortedArray = SortedArray.sort();
         foreach (Id => Score in SortedArray){
            SortedArray[Id] = MathLib::Abs(Score);
         }
      }
      case CTmResult::ETmRaceResultCriteria::NbRespawns:
         SortedArray = _RoundArrivalArray.sort();
      default:
         assert(False); //Shouldn't reach this point.
   }
   return SortedArray;
}


Integer GetEventScore(CTmModeEvent Event,  CTmResult::ETmRaceResultCriteria ResultCriteria) {
   declare Integer Result;
   switch (ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time:
         Result = Event.RaceTime;
      case CTmResult::ETmRaceResultCriteria::Stunts:
         Result = Event.StuntsScore;
      case CTmResult::ETmRaceResultCriteria::NbRespawns:
         Result = Event.NbRespawns;
      default:
         assert(False); //Shouldn't be here! FIx it!
   }
   return Result;
}


Void ClearScoreTimes() {
   foreach (Score in Scores) {
      Score.BestRace.Time = -1;
      Score.BestLap.Time = -1;
      Score.PrevRace.Time = -1;
      Score.TempResult.Time = -1;
      
      Score.BestRace.StuntsScore = 0;
      Score.BestLap.StuntsScore = 0;
      Score.PrevRace.StuntsScore = 0;
      Score.TempResult.StuntsScore = 0;
      
      Score.BestRace.NbRespawns = -1;
      Score.BestLap.NbRespawns = -1;
      Score.PrevRace.NbRespawns = -1;
      Score.TempResult.NbRespawns = -1;
   }
}

Boolean IsPointsTied() {
   if (Scores.count > 1)
      return Scores[0].Points == Scores[1].Points;

   return False;
}

Integer CalcStuntScoreAtCountDown(CTmPlayer Player, Integer Time) {
   declare StuntScore = Player.CurRace.StuntsScore;
   if (Player.RaceStartTime != 0 && Time > STUNT_MODE_COUNT_DOWN) {
      StuntScore = StuntScore - (Time - STUNT_MODE_COUNT_DOWN) / 200;
      if (StuntScore < 0)
         StuntScore = 0;
   }
   return StuntScore;
}

Boolean IsEndOfMatchReached(Integer MatchRoundNr) {
   if (MatchMode == MATCH_MODE_POINTS && Scores.count > 0 && Scores[0].Points >= PointsLimit)
      return True;
   else if (MatchMode == MATCH_MODE_NR_OF_ROUNDS && MatchRoundNr >= NrOfRoundsLimit)
      return True;
   else if (MatchMode == MATCH_MODE_PLAYLIST && MatchRoundNr >= MapList.count)
      return True;
   return False;
}

Void UnSpawn(CTmPlayer Player) {
   TM::EndRaceSequence_Remove(Player);
   Player.RaceStartTime = 0;
}

Integer[] CreateMapOrder(Boolean shouldRandomize) {
   declare Integer[] newMapOrder;
   declare Integer maxMapNr = MapList.count-1;
   
   for(i,0,maxMapNr) {
      newMapOrder.add(i);
   }
   
   if (shouldRandomize) {
      declare Integer tmp;
      declare Integer rand;
      
      for (i, 0, maxMapNr) {
         rand = MathLib::Rand(0, maxMapNr);
         tmp = newMapOrder[i];
         newMapOrder[i] = newMapOrder[rand];
         newMapOrder[rand] = tmp;
      }
   }
   return newMapOrder;
}

Text GetRaceResultText(Integer Result, CTmResult::ETmRaceResultCriteria ResultCriteria) {
   declare Text RaceResultText;
   switch (ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time: {
         RaceResultText ^= TextLib::TimeToText(Result, True);
      }
      case CTmResult::ETmRaceResultCriteria::Stunts: {
         RaceResultText ^= Result ^" Points";
      }
      default:
         assert(False);  //Should not be here
   }
   return RaceResultText;
}

Text GetRaceResultText(CTmResult Result, CTmResult::ETmRaceResultCriteria ResultCriteria) {
   declare Text RaceResultText;
   RaceResultText = GetRaceResultText(GetResultScore(Result, ResultCriteria), ResultCriteria);
   return RaceResultText;
}





Text GetManiaLinkPage(Text text, Integer length, Integer height) {
return """<frame posn="0 60">
         <quad  posn="0 3 -1"   sizen="{{{length}}} {{{height}}}"   halign="center"   style="Bgs1InRace" substyle="BgTitle3"  />
         <label posn="0 0"                  halign="center" scale="1" text="{{{text}}}" />
         </frame>""";
}

Text GetManiaLinkPage(Text text) {
   return GetManiaLinkPage(text, 15, 2);
}

Void InitStuntFigureDictionary() {
   StuntFigureDictionary.clear();
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::None] =                "None???";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::StraightJump] =          "Jump";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Flip] =                "Flip";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::BackFlip] =             "Back Flip";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Spin] =                "Spin";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Aerial] =             "Aerial";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::AlleyOop] =             "Alley Ooop";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Roll] =                "Roll";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Corkscrew] =             "Corkscrew";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::SpinOff] =             "Spin Off";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Rodeo] =                "Rodeo";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::FlipFlap] =             "Flip Flap";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Twister] =             "Twister";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::FreeStyle] =             "Free Style";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::SpinningMix] =          "Spinning Mix";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::FlippingChaos] =          "Flipping Chaos";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::RollingMadness] =       "Rolling Madness";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckNone] =             "Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckStraightJump] =       "Jump Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFlip] =             "Flip Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckBackFlip] =          "Back Flip Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckSpin] =             "Spin Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckAerial] =          "Aerial Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckAlleyOop] =          "Alley Ooop Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckRoll] =             "Roll Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckCorkscrew] =       "Corkscrew Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckSpinOff] =          "Spin Off Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckRodeo] =          "Rodeo Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFlipFlap] =          "Flip Flap Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckTwister] =          "Twister Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFreeStyle] =       "Free Style Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckSpinningMix] =       "Spinning Mix Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckFlippingChaos] =    "Flipping Chaos Wreck";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::WreckRollingMadness] =   "Rolling Madness Wreck";
///
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::TimePenalty] =          "Time Penalty";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Reset] =                "Reset";
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::RespawnPenalty] =       "Respawn Penalty";
///
   StuntFigureDictionary[CTmModeEvent::EStuntFigure::Grind] =                "Grind";
}

Text GetStuntText(CTmModeEvent Event) {
   declare Text StuntText = "";
   
   /*
   (Chained)(Master) (Straight) (Basic) xxx (Angle)(!!!)
      + 34
      50 Points
   */
   
   if (Event.Combo > 0) {
      if (Event.Combo > 1)
         StuntText ^= """{{{Event.Combo}}}X""";
      StuntText ^= " Chained";
   }
   
   if (Event.IsReverse)
	  StuntText ^= " Reverse";
   if (Event.IsMasterJump) 
      StuntText ^= " Master";
   if (Event.IsStraight)
      StuntText ^= " Straight";
   if (Event.Angle == 0)
      StuntText ^= " Basic";
   
   StuntText ^=""" {{{StuntFigureDictionary[Event.StuntFigure]}}}""";
   
   if (Event.Angle != 0) {
      StuntText ^= """ {{{Event.Angle}}}""";
      for (i, 1, Event.Angle/180)
         StuntText ^= "!";
   }
   return StuntText;
}



Void UpdateStuntScoreUI(CTmPlayer Player, Text StuntFigureText, Integer StuntPoint, Integer Score) {
   declare UI <=> UIManager.GetUI(Player);
   
   if (UI != Null) {
      declare Text PointIncrText;
	  declare Text plusminus;
	  plusminus="-";
      if (StuntPoint > 0) {
	  	if (StuntFigureText != " Basic Respawn Penalty") {
		plusminus="+";
	}
         PointIncrText = plusminus^"""{{{StuntPoint}}}""";
      }
      
      UI.StatusMessage = """{{{StuntFigureText}}}
{{{PointIncrText}}}
{{{Score}}} Points""";
   }
}

Void UpdateStuntScoreUI(CTmPlayer Player, Text StuntFigureText, Integer StuntPoint) {
	  UpdateStuntScoreUI(Player, StuntFigureText, StuntPoint, Player.CurRace.StuntsScore);
}
/* Not used. (Need to rewrite it in order to improve stunt mode ui.)
Void doUI(CPlayer _Player) {
   
   declare UI <=> UIManager.GetUI(_Player);
   declare CUILayer UILayerTmp;
   
   if ( UI != Null ) {
      UI.UILayers.add(UILayerTmp);
      //frame pos 80 -45 = lower right quadrant
      UILayerTmp.ManialinkPage = """<frame posn="0 0" >
         <!-- <quad  posn="0 0 -1" valign="center" halign="center"  sizen="160 90"    style="Bgs1InRace" substyle="BgTitle3"  /> -->
         <label posn="0 0"    valign="center"          halign="center"     scale="2" text="Stunt: " />
         </frame>""";
      
   }   
}
*/

Void logUI(CUIConfig UI, Text _text) {
   log(_text ^" UIStatus: " ^UI.UIStatus);
   log("Number of layers: "^UI.UILayers.count);
   //log("IsHidden: "^UI.OverlayHideNotices ^ UI.OverlayHideMapInfo^UI.OverlayHideOpponentsInfo^UI.OverlayHideChat^UI.OverlayHideCheckPointList^UI.OverlayHideRoundScores^UI.OverlayHideAll^UI.OverlayHideScoresOnAltMenu^UI.NoticesFilter_HidePlayerInfo^UI.NoticesFilter_HidePlayerWarning^UI.NoticesFilter_HidePlayerInfoIfNotMe^UI.NoticesFilter_HidePlayerWarningIfNotMe^UI.NoticesFilter_HideMapInfo^UI.NoticesFilter_HideMapWarning^UI.NoticesFilter_HideMatchInfo^UI.NoticesFilter_HideMatchWarning);
   log("ScoreTableVisibility: "^UI.ScoreTableVisibility^" SmallScoreTableVisibility: "^UI.SmallScoreTableVisibility^"    AlliesLabelsVisibility: "^UI.AlliesLabelsVisibility^" EnemiesLabelsVisibility: "^UI.EnemiesLabelsVisibility^" EnemiesLabelsShowGauges: "^UI.EnemiesLabelsShowGauges);
   log("ManiaLinkPage :"^ UI.ManialinkPage);
}

//Best times shown in upper left corner 
Text GetFrameMapResults(CTmResult::ETmRaceResultCriteria ResultCriteria)
{
   declare Integer[Text] Results;
   if (ShowGoldTime && ResultCriteria == CTmResult::ETmRaceResultCriteria::Time) {
      Results["Gold"] = Map.TMObjective_GoldTime;
   }
   foreach (Score in Scores) {
      switch (ResultCriteria) {
         case CTmResult::ETmRaceResultCriteria::Time: {
            if (Score.BestRace.Time != -1)
               Results[Score.User.Name] = Score.BestRace.Time;
         }
         case CTmResult::ETmRaceResultCriteria::Stunts: {
            if (Score.BestRace.StuntsScore != -1)
               Results[Score.User.Name] = Score.BestRace.StuntsScore;
         }
         default:
            assert(False); //Should not reach this point
      }
   }
   
   Results = SortResultArray(Results, ResultCriteria);
   declare Text Legend;
   switch (ResultCriteria) {
      case CTmResult::ETmRaceResultCriteria::Time:
         Legend = ("$oBest times:");
      case CTmResult::ETmRaceResultCriteria::Stunts:
         Legend = ("$oBest scores:");
      default:
         assert(False); //Shouldn't reach this point
   }
   
   declare Frame = """   <frame posn="-160 76">
                  <label posn="1 3"    style="TextRaceStaticSmall" sizen="50 2" halign="left" text="{{{Legend}}}"  />""";
   declare Line = 0;
   foreach (Name => Result in Results) {
      Frame ^= """<label posn="2 {{{-3*Line}}}"    style="TextPlayerCardName"  sizen="25 3" halign="left" text="{{{Name}}}"  />
                  <label posn="30 {{{-3*Line}}}"    style="TextRaceStaticSmall" sizen="25 3" halign="left" text="$0F0{{{GetRaceResultText(Result, ResultCriteria)}}}"  />""";
      Line += 1;
   }
   
   Frame ^=  """</frame>""" ;
   
   return Frame;
}

declare CUILayer UILayerScores;
declare CUILayer UILayerInfo;


//  ============ Intro Sequence:
Void PlayIntroSequence(Text InfoText){
   sleep(0);   // flush loading lag.
   UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
   UIManager.UIAll.UILayers.clear();
   UIManager.UIAll.UILayers.add(UILayerScores);
   
   UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
   UIManager.UIAll.UISequence = CUIConfig::EUISequence::Intro;
   
   //sleep(4000);   // HACK; should be "wait(sequences over)"
   wait(UIManager.UIAll.UISequenceIsCompleted);
   
   if (InfoText == "")
      UILayerInfo.ManialinkPage = "";
   else {
      UILayerInfo.ManialinkPage = GetManiaLinkPage(InfoText);
   }
   UIManager.UIAll.UILayers.add(UILayerInfo);
   
   UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
   UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
   foreach(Player, Players) {
      declare UI <=> UIManager.UI[Player];
      Player.RaceStartTime = Now + 100000;         // spawn the cars.
   }
   sleep(3000);
   
   //Remove UILayerInfo
   UIManager.UIAll.UILayers.clear();
   UIManager.UIAll.UILayers.add(UILayerScores);
   
   UIManager.UIAll.BigMessage = "";
   UIManager.UIAll.StatusMessage = "";
   foreach(Player, Players) {
      declare UI <=> UIManager.UI[Player];
      UI.BigMessage = "";
      UI.BigMessageAvatarLogin = "";
      UI.StatusMessage = "";
   }
}


// === Race rounds ==
Void DoTimeAttackRace()
{
   UiRounds = False;
   
   
   if (GameMode == GAME_MODE_STUNTS) {  //Should be stuntmodeui= true
      UiStuntsMode = True;
      UiRaceChrono = CTmMode::ETmRaceChronoBehaviour::CountDown;
      UiDisplayStuntsNames = True; //Nadeo hasn't got it working yet. Extracted own stunt name  own StuntUI Layer.
      ResultCriteria = CTmResult::ETmRaceResultCriteria::Stunts;
   } else { //StuntMode Off
      ResultCriteria = CTmResult::ETmRaceResultCriteria::Time;
   }
   UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing;
   
   TM::Players_SpawnAll(Now + 2000);
   foreach(Score, Scores) {
      Score.PrevRaceDeltaPoints = 0;
   }
   sleep(500);   
   Synchro_DoBarrier();

   RoundArrivalOrder = Integer[Ident];
   declare RoundArrivalOrderDirty = True; 
   declare LastRespawnTime = Integer[Ident];
   declare CheckpointCount = Integer[Ident];
   foreach (Player, Players) {
      LastRespawnTime[Player.Id] = Now;
   }
   declare Integer MatchBestResult for Map = -1;
   
   
   foreach(Player, Players) {
      declare Integer StuntUIState for Player;
      Player.CurRace.StuntsScore = 0;
//      StuntsScore[Player.Id] = 0;
      StuntUIState = REGULAR_SCORE;  //Is this one needed?
   }
   
   
   UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
   
   CutOffTimeLimit = Now + TimeLimitSeconds * 1000 +2500;
   
   //=== Loop until all players finished racing
   declare NbContestants = Players.count;
   while(  Now < CutOffTimeLimit ) {
      TM::RunGame();
      TM::EndRaceSequence_Update();
      
      if (MatchEndRequested)
         return;

      // Process events queue
      foreach(Event, PendingEvents) {         
         PassOn(Event);
		 declare Text stunttext = "";
         declare Player <=> Event.Player;
         declare Integer StuntUIState for Player;
         stunttext=GetStuntText(Event);
         if (Event.Type == CTmModeEvent::EType::Stunt) {
		XmlRpc.SendCallback("playerStunt", "playerLogin:"^Player.User.Login^";stuntScore:"^Player.CurRace.StuntsScore^";eventScore:"^Event.Points^";Stunt:"^stunttext^":");
			if ( stunttext == " Basic Respawn Penalty" ) {
			Player.CurRace.StuntsScore -= Event.Points; }
		 else {
            Player.CurRace.StuntsScore += Event.Points;
			 }
			
            
			UpdateStuntScoreUI(Event.Player, GetStuntText(Event), Event.Points);         
            StuntUIState = Now + STUNT_TEXT_TIMEOUT;
         }
         
           else if (Event.Type == CTmModeEvent::EType::StartLine) {
            CheckpointCount[Player.Id] = 0;
            StuntUIState = REGULAR_SCORE; //Displays stuntui when respawned
            Player.CurRace.StuntsScore = 0;
         } else if (Event.Type == CTmModeEvent::EType::WayPoint) {
            CheckpointCount[Player.Id] +=1;
		//xmlrpc
		declare Text Stuntdata= "playerLogin:"^Player.User.Login^";stuntScore:"^Player.CurRace.StuntsScore;
		XmlRpc.SendCallback("playerCheckpoint", Stuntdata);
            if (Event.IsEndRace) {
               assert(Player.CurRace.Time == Event.RaceTime);
               Player.CurRace.StuntsScore = CalcStuntScoreAtCountDown(Player, Event.RaceTime);
               StuntUIState = HAS_FINISHED; //Stops the countdown in StuntUI and displays the final score.
          
               Player.Score.PrevRace = Player.CurRace;
			   declare Text ResultText = "$00F" ^ GetRaceResultText(Player.CurRace, ResultCriteria);
			   declare NbRespawns = Player.CurRace.NbRespawns;
		//xmlrpc					
		XmlRpc.SendCallback("playerFinish", "playerLogin:"^Player.User.Login^";stuntScore:"^Player.CurRace.StuntsScore^";CPCount:"^NbRespawns);             
               if (IsResultBetter(Player.CurRace, Player.Score.BestRace, ResultCriteria)) {
                  Player.Score.BestRace = Player.CurRace;
				  
                  if (IsResultBetter(GetResultScore(Player.CurRace, ResultCriteria), MatchBestResult, ResultCriteria)) {
                     MatchBestResult = GetResultScore(Player.CurRace, ResultCriteria);
                     TM::EndRaceSequence_Add(Player, TextLib::Compose(_("New match record! %1"), ResultText));
                  } else {
                     TM::EndRaceSequence_Add(Player, TextLib::Compose(_("Personal record! %1"), ResultText));
                  }
                  RoundArrivalOrder[Player.Id] = GetEventScore(Event, ResultCriteria);
                  RoundArrivalOrderDirty = True;
               } else {
                  if (GameMode != GAME_MODE_STUNTS)
                     ResultText = "";
                  TM::EndRaceSequence_Add(Player, ResultText);
               }
            }
         }
      }
      
      //Not fully optimized, could have a proper Dirty flag...
      foreach(Player, Players) {
         declare Integer StuntUIState for Player;
      
         //assert (Player.Id!= prevPlayerId);
         if ((StuntUIState > 0 && Now > StuntUIState)
               || StuntUIState == REGULAR_SCORE) {
            UpdateStuntScoreUI(Player, "", 0);
            StuntUIState = SKIP_REDRAW;
            
         } else if (StuntUIState == HAS_FINISHED) {
            //UpdateStuntScoreUI(Player, "", 0);
         }
         
         
         declare CurRaceTime = Now - Player.RaceStartTime;
         if (Player.RaceStartTime > 0 && CurRaceTime > STUNT_MODE_COUNT_DOWN
                && StuntUIState != HAS_FINISHED) {
            UpdateStuntScoreUI(Player, "$f00Finish Now!", 0, CalcStuntScoreAtCountDown(Player, CurRaceTime));
         }
         //prevPlayerId = Player.Id;
      }

      // Refresh score table.      
      if (RoundArrivalOrderDirty) {
         RoundArrivalOrderDirty = False;
         RoundArrivalOrder = SortRoundArrivalOrder(RoundArrivalOrder, ResultCriteria);
                  
         UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);      // refresh scores...
      }
      
      // Respawn unspawned players
      if (PlayersWaiting.count > 0) {
         TM::Players_SpawnWaiting(0);
      }
   }
   TM::Players_UnspawnAll();
   
   Synchro_DoBarrier();

   // endround sequence
   sleep(1000);
}

Void PlayEndOfRoundSequence(Text InfoText, Integer MatchRoundNr) {
      UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound;
   UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
   
   sleep(6500);   //Showing race times End of round
   
   if (MatchMode != MATCH_MODE_OFF) {   //Display match point sequence
      UiRounds = False;
	  UiStuntsMode = True; 
      
      if (InfoText == "")
         UILayerInfo.ManialinkPage = "";
      else {
         UILayerInfo.ManialinkPage = GetManiaLinkPage(InfoText);
      }
      UIManager.UIAll.UILayers.add(UILayerInfo);

      declare Index = 0;
      declare Text Table;
      switch(ResultCriteria) {
         case CTmResult::ETmRaceResultCriteria::Time:
            Table = """<scoretable type="time">""";
         case CTmResult::ETmRaceResultCriteria::Stunts:
            Table = """<scoretable type="stunts">""";
         case CTmResult::ETmRaceResultCriteria::NbRespawns:
            assert(False); //Not implemented yet
         default:
            assert(False); //Shouldn't reach this point.
      }   
      declare Ident PrevPlayerId = NullId;
      declare ScoreInc = 0;
      foreach (PlayerId => Score in RoundArrivalOrder) {
         declare Player <=> Players[PlayerId];      // faut le PlayerInfo
         
         //Only update score of previous player if the times aren't identical
         declare Boolean IsSameTimeAsPrevPlayer = (PrevPlayerId != NullId && Score == RoundArrivalOrder[PrevPlayerId]);
         
         if (!IsSameTimeAsPrevPlayer) {
            if (Index < RoundPoints.count)
               ScoreInc = RoundPoints[Index];
            else
               ScoreInc = 1;
            
            if (Index == Players.count -1)      // last players gets no points.
               ScoreInc = 0;            
         }
         Index += 1;
         
         Table ^= """<score login="{{{Player.Login}}}" value="{{{Score}}}" inc="{{{ScoreInc}}}"/>""";      
         Player.Score.PrevRaceDeltaPoints = ScoreInc;
         PrevPlayerId = PlayerId;
      }
      Table ^= """</scoretable>""";
      UIManager.UIAll.SmallScoreTable = Table;

      sleep(12000);   //Showing DeltaPoints

      foreach(Score, Scores) {
         Score.Points += Score.PrevRaceDeltaPoints;
         Score.PrevRaceDeltaPoints = 0;
      }
      
      if (IsPointsTied() && IsEndOfMatchReached(MatchRoundNr))
         UILayerInfo.ManialinkPage = GetManiaLinkPage("Points are tied! Next round will be a tiebreaker!", 210, 14);
      
      sleep(2500); //Showing TotalPoints
      UILayerInfo.ManialinkPage = "";
   }
   
   UIManager.UIAll.SmallScoreTable = "";
   UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
   
   sleep(500);   
   Synchro_DoBarrier();
}


//==== EndOfMatchSequence ====
Void EndOfMatchSequence() {
   TM::Players_UnspawnAll();
   UiRounds = True;
   
   if (Scores.count > 0 && Scores[0].Points > 0 && !ServerShutdownRequested) {
      UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
      UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium;
      sleep(12500);
      
      declare Text WinnerNames = Scores[0].User.Name;
      declare TextBoxWidth = 130;
      
      for (i, 1, Scores.count -1) {
         if ( Scores[i].Points == Scores[0].Points) {
            WinnerNames ^= TextLib::MLEncode(" & ") ^Scores[i].User.Name;
            TextBoxWidth = 320;
         }
         else 
            break;
      }
      
      
      declare VictoryText = TextLib::Compose(_("$<%1$> wins the match!"), WinnerNames);
      UILayerScores.ManialinkPage = GetFrameMapResults(ResultCriteria);
      UILayerInfo.ManialinkPage = GetManiaLinkPage(VictoryText, TextBoxWidth, 14);
      

      UIManager.UIAll.UILayers.clear();
      UIManager.UIAll.UILayers.add(UILayerScores);
      UIManager.UIAll.UILayers.add(UILayerInfo);

      sleep(3000);
      UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
      sleep(5000);
   }
   
   Synchro_DoBarrier();

   UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal;
   UIManager.UIAll.UILayers.clear();

   Scores_Clear();
   
   MatchEndRequested = False;      // taken into account.
}

// === Main ===
main() 
{
   //Init
   UIManager.ResetAll();
   log("restart...");
   
   InitializeGlobalVars();
   
   
   IndependantLaps = True;
   
   RespawnBehaviour = ::ETMRespawnBehaviour::Normal;

   UILayerScores <=> UIManager.UILayerCreate();
   UILayerInfo <=> UIManager.UILayerCreate();
   
   InitStuntFigureDictionary();

   MapOrder = CreateMapOrder(RandomMapOrder);
   declare MapOrderIndex = 0;
   
   //Since the script needs to be pasted instead of loaded there is already a loaded map.
   //Need to unload it to enforce map order.
   //if (MapLoaded)
   //   UnloadMap();
   
   // loop until server interrupted
   while( !ServerShutdownRequested ) {
      declare MatchRoundNr = 1;
      declare IsTieBreakRound = False;
      
      declare IntroText = "";
      declare EndOfRoundText = "";
      
      //make sure new match start with first map in maplist in playlist mode. (Last match could've ended in tiebreak.)
      if (MatchMode == MATCH_MODE_PLAYLIST) {
         MapOrderIndex = 0;
      }
      
      // ============ Play the Time Attack races until the end of match condition is reached:
      while( !MatchEndRequested ) {
         IsSkipThisRound = False;
         
         //Display scoreTable before match in Rounds mode if a Match Mode is set
         if (MatchMode!= MATCH_MODE_OFF)
            UiRounds = True;
         UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible;
         UIManager.UIAll.UISequence = CUIConfig::EUISequence::Intro;

         NextMapIndex = MapOrder[MapOrderIndex];

         LoadMap();

         //ClearScoreTimes();  //Don't call "Scores_Clear" because we want to preserve the match points
         Scores_Clear();
         TM::Players_UnspawnAll();
         
         
         //Set texts according to settings and state
         if (IsTieBreakRound) {
            IntroText = "Tiebreaker round!";
            EndOfRoundText = "Player with most points wins!";
         } else if (MatchMode == MATCH_MODE_POINTS) {
            if (MatchRoundNr == 1)
               IntroText = "New match!";
            else
               IntroText = "";
            EndOfRoundText = """{{{PointsLimit}}} points to win!""";
         } else if (MatchMode == MATCH_MODE_NR_OF_ROUNDS) {
            IntroText = """Round {{{MatchRoundNr}}} of {{{NrOfRoundsLimit}}}""";
            EndOfRoundText = """Round {{{MatchRoundNr}}} of {{{NrOfRoundsLimit}}}""";
         } else if (MatchMode == MATCH_MODE_PLAYLIST) {
            IntroText = """Round {{{MatchRoundNr}}} of {{{MapList.count}}}""";
            EndOfRoundText = """Round {{{MatchRoundNr}}} of {{{MapList.count}}}""";
         }
         
         PlayIntroSequence(IntroText);
         DoTimeAttackRace();
         if (!IsSkipThisRound)
            PlayEndOfRoundSequence(EndOfRoundText, MatchRoundNr);
         
         if (IsEndOfMatchReached(MatchRoundNr)) {
            if (!CanMatchEndInTie && IsPointsTied())
               IsTieBreakRound = True;
            else {
               IsTieBreakRound = False;
               break;       // score limit reached.
            }
         }
         
         if (!IsSkipThisRound || MatchMode == MATCH_MODE_PLAYLIST)
            MatchRoundNr += 1;
         
         MapOrderIndex += 1;
         if (MapOrderIndex >= MapList.count) {
            MapOrderIndex = 0;
         }
         
         UnloadMap();
      }
      
      // ============ End of Match Sequence:
      if (MatchMode != MATCH_MODE_OFF)
         EndOfMatchSequence();
      
      UnloadMap();
   }
   
   // ====== Cleanup
   log("Cleanup! (Is this bit of code ever called???)");
   UIManager.UILayerDestroy(UILayerScores);
   UIManager.UILayerDestroy(UILayerInfo);
}

Re: SplitScreen Stunt Mode Script V0.6

Posted: 11 Sep 2012, 16:26
by supersmo
SplitScreen Stunt Mode Script V0.6

Changelog
V0.6
  • Really fixed score sorting at end of match this time. ;)
  • Some code cleanup

Re: SplitScreen Stunt Mode Script V0.5

Posted: 11 Sep 2012, 16:31
by supersmo
kapsubm wrote:the script so far (works perfectly in splitscreen, single and multiplayermode)
(issues: Scoretable on end of round)
the Difference to your script is, that u can respan, the time limit is taken in account,
in this time all players can do stunts as long as the time limit is not null
(scoring 1 minute due to nadeo restriction)
if we can sort out the table sort and maybe ladderranks, it should be ready for release.
Cool :)
I fixed the sorting of stunt scores in the splitscreen script.

Put Scores_Sort(::ETmRaceResultOrder::BestRace_Stunts) right after Player.Score.BestRace = Player.CurRace;
And it should be fixed in your version of the script as well.