7d436ac0c5
* Fix trying to write to a struct before its initialized. Same issue on IW4, IW5 and T5 game modules. * Fix path issues in the scripts + add support for t5zm. * Fix deploy.bat * Change paths inside the gsc scripts used to call functions in other scripts * Remove mp includes from base gsc file. #include maps\mp\_utility; #include maps\mp\gametypes\_hud_util; * Define GetXuid as overrideMethod as t5zm doesn't have it. * Define GetPlayerFromClientNum as getting all players is slightly different on t5zm. * Remove the precompiled gsc file for T6 as PlutoT6 can load uncompiled GSC now. * Fix _customcallbacks.gsc for T6 * Add T6 support to the game interface. * Update _integration_base.gsc use camelCase for functionName * Make sure the Setup functions are always called in the right order. Base -> shared -> game Otherwise we might write to structs before they are created. * Move functions interacting with the game from _base to _shared GetPlayerFromClientNum OnPlayerJoinedTeam OnPlayerJoinedSpectators GenerateJoinTeamString PlayerTrackingOnInterval SaveTrackingMetrics * Block execution until game specific setup is done Block _shared execution until the game specific file finished. This allows the game specific file to override the events in _shared. * Fix setup event flow Move check of sv_iw4madmin_integration_enabled dvar after waittill in _shared so _base has a chance to set it to 1. Move check of sv_iw4madmin_autobalance dvar to OnPlayerConnect in _shared so the game specific script has a chance to set the dvar. * ignore bots * add more spaces
588 lines
17 KiB
Plaintext
588 lines
17 KiB
Plaintext
|
|
Init()
|
|
{
|
|
thread Setup();
|
|
}
|
|
|
|
Setup()
|
|
{
|
|
level endon( "game_ended" );
|
|
|
|
// it's possible that the notify type has not been defined yet so we have to hard code it
|
|
level waittill( "IntegrationBootstrapInitialized" );
|
|
|
|
level.commonFunctions.changeTeam = "ChangeTeam";
|
|
level.commonFunctions.getTeamCounts = "GetTeamCounts";
|
|
level.commonFunctions.getMaxClients = "GetMaxClients";
|
|
level.commonFunctions.getTeamBased = "GetTeamBased";
|
|
level.commonFunctions.getClientTeam = "GetClientTeam";
|
|
level.commonFunctions.getClientKillStreak = "GetClientKillStreak";
|
|
level.commonFunctions.backupRestoreClientKillStreakData = "BackupRestoreClientKillStreakData";
|
|
level.commonFunctions.waitTillAnyTimeout = "WaitTillAnyTimeout";
|
|
|
|
level.overrideMethods[level.commonFunctions.changeTeam] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.getTeamCounts] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.getTeamBased] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.getMaxClients] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.getClientTeam] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.getClientKillStreak] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = scripts\_integration_base::NotImplementedFunction;
|
|
level.overrideMethods["GetPlayerFromClientNum"] = ::GetPlayerFromClientNum;
|
|
|
|
// these can be overridden per game if needed
|
|
level.commonKeys.team1 = "allies";
|
|
level.commonKeys.team2 = "axis";
|
|
level.commonKeys.teamSpectator = "spectator";
|
|
|
|
level.eventTypes.connect = "connected";
|
|
level.eventTypes.disconnect = "disconnect";
|
|
level.eventTypes.joinTeam = "joined_team";
|
|
level.eventTypes.spawned = "spawned_player";
|
|
level.eventTypes.gameEnd = "game_ended";
|
|
|
|
level.iw4madminIntegrationDefaultPerformance = 200;
|
|
|
|
level notify( level.notifyTypes.sharedFunctionsInitialized );
|
|
level waittill( level.notifyTypes.gameFunctionsInitialized );
|
|
|
|
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
level thread OnPlayerConnect();
|
|
}
|
|
|
|
OnPlayerConnect()
|
|
{
|
|
level endon( level.eventTypes.gameEnd );
|
|
|
|
for ( ;; )
|
|
{
|
|
level waittill( level.eventTypes.connect, player );
|
|
|
|
if ( scripts\_integration_base::_IsBot( player ) )
|
|
{
|
|
// we don't want to track bots
|
|
continue;
|
|
}
|
|
|
|
player thread OnPlayerJoinedTeam();
|
|
player thread OnPlayerJoinedSpectators();
|
|
player thread PlayerTrackingOnInterval();
|
|
|
|
if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 || !IsDefined( [[level.overrideMethods[level.commonFunctions.getTeamBased]]]() ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( ![[level.overrideMethods[level.commonFunctions.getTeamBased]]]() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
teamToJoin = player GetTeamToJoin();
|
|
player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin );
|
|
|
|
player thread OnClientFirstSpawn();
|
|
player thread OnClientJoinedTeam();
|
|
player thread OnClientDisconnect();
|
|
}
|
|
}
|
|
|
|
OnClientDisconnect()
|
|
{
|
|
level endon( level.eventTypes.gameEnd );
|
|
self endon( "disconnect_logic_end" );
|
|
|
|
for ( ;; )
|
|
{
|
|
self waittill( level.eventTypes.disconnect );
|
|
scripts\_integration_base::LogDebug( "client is disconnecting" );
|
|
|
|
OnTeamSizeChanged();
|
|
self notify( "disconnect_logic_end" );
|
|
}
|
|
}
|
|
|
|
OnClientJoinedTeam()
|
|
{
|
|
self endon( level.eventTypes.disconnect );
|
|
|
|
for( ;; )
|
|
{
|
|
self waittill( level.eventTypes.joinTeam );
|
|
|
|
if ( IsDefined( self.wasAutoBalanced ) && self.wasAutoBalanced )
|
|
{
|
|
self.wasAutoBalanced = false;
|
|
continue;
|
|
}
|
|
|
|
newTeam = self [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
|
|
scripts\_integration_base::LogDebug( self.name + " switched to " + newTeam );
|
|
|
|
if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 )
|
|
{
|
|
OnTeamSizeChanged();
|
|
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
|
|
continue;
|
|
}
|
|
|
|
properTeam = self GetTeamToJoin();
|
|
if ( newTeam != properTeam )
|
|
{
|
|
self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
|
|
self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( properTeam );
|
|
wait ( 0.1 );
|
|
self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
|
|
}
|
|
}
|
|
}
|
|
|
|
OnClientFirstSpawn()
|
|
{
|
|
self endon( level.eventTypes.disconnect );
|
|
timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.spawned );
|
|
|
|
if ( timeoutResult != "timeout" )
|
|
{
|
|
return;
|
|
}
|
|
|
|
scripts\_integration_base::LogDebug( "moving " + self.name + " to spectator because they did not spawn within expected duration" );
|
|
self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( level.commonKeys.teamSpectator );
|
|
}
|
|
|
|
OnTeamSizeChanged()
|
|
{
|
|
if ( level.players.size < 3 )
|
|
{
|
|
scripts\_integration_base::LogDebug( "not enough clients to autobalance" );
|
|
return;
|
|
}
|
|
|
|
if ( !IsDefined( GetSmallerTeam( 1 ) ) )
|
|
{
|
|
scripts\_integration_base::LogDebug( "teams are not unbalanced enough to auto balance" );
|
|
return;
|
|
}
|
|
|
|
toSwap = FindClientToSwap();
|
|
curentTeam = toSwap [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
|
|
otherTeam = level.commonKeys.team1;
|
|
|
|
if ( curentTeam == otherTeam )
|
|
{
|
|
otherTeam = level.commonKeys.team2;
|
|
}
|
|
|
|
toSwap.wasAutoBalanced = true;
|
|
|
|
if ( !IsDefined( toSwap.autoBalanceCount ) )
|
|
{
|
|
toSwap.autoBalanceCount = 1;
|
|
}
|
|
else
|
|
{
|
|
toSwap.autoBalanceCount++;
|
|
}
|
|
|
|
toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
|
|
scripts\_integration_base::LogDebug( "swapping " + toSwap.name + " from " + curentTeam + " to " + otherTeam );
|
|
toSwap [[level.overrideMethods[level.commonFunctions.changeTeam]]]( otherTeam );
|
|
wait ( 0.1 ); // give the killstreak on team switch clear event time to execute
|
|
toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
|
|
}
|
|
|
|
FindClientToSwap()
|
|
{
|
|
smallerTeam = GetSmallerTeam( 1 );
|
|
teamPool = level.commonKeys.team1;
|
|
|
|
if ( IsDefined( smallerTeam ) )
|
|
{
|
|
if ( smallerTeam == teamPool )
|
|
{
|
|
teamPool = level.commonKeys.team2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
teamPerformances = GetTeamPerformances();
|
|
team1Perf = teamPerformances[level.commonKeys.team1];
|
|
team2Perf = teamPerformances[level.commonKeys.team2];
|
|
teamPool = level.commonKeys.team1;
|
|
|
|
if ( team2Perf > team1Perf )
|
|
{
|
|
teamPool = level.commonKeys.team2;
|
|
}
|
|
}
|
|
|
|
client = GetBestSwapCandidate( teamPool );
|
|
|
|
if ( !IsDefined( client ) )
|
|
{
|
|
scripts\_integration_base::LogDebug( "could not find candidate to swap teams" );
|
|
}
|
|
else
|
|
{
|
|
scripts\_integration_base::LogDebug( "best candidate to swap teams is " + client.name );
|
|
}
|
|
|
|
return client;
|
|
}
|
|
|
|
GetBestSwapCandidate( team )
|
|
{
|
|
candidates = [];
|
|
maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();
|
|
|
|
for ( i = 0; i < maxClients; i++ )
|
|
{
|
|
candidates[i] = GetClosestPerformanceClientForTeam( team, candidates );
|
|
}
|
|
|
|
candidate = undefined;
|
|
|
|
foundCandidate = false;
|
|
for ( i = 0; i < maxClients; i++ )
|
|
{
|
|
if ( !IsDefined( candidates[i] ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
candidate = candidates[i];
|
|
candidateKillStreak = candidate [[level.overrideMethods[level.commonFunctions.getClientKillStreak]]]();
|
|
|
|
scripts\_integration_base::LogDebug( "candidate killstreak is " + candidateKillStreak );
|
|
|
|
if ( candidateKillStreak > 3 )
|
|
{
|
|
scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " because their kill streak is too high" );
|
|
continue;
|
|
}
|
|
|
|
if ( IsDefined( candidate.autoBalanceCount ) && candidate.autoBalanceCount > 2 )
|
|
{
|
|
scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " they have been swapped too many times" );
|
|
continue;
|
|
}
|
|
|
|
foundCandidate = true;
|
|
break;
|
|
}
|
|
|
|
if ( foundCandidate )
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
GetClosestPerformanceClientForTeam( sourceTeam, excluded )
|
|
{
|
|
if ( !IsDefined( excluded ) )
|
|
{
|
|
excluded = [];
|
|
}
|
|
|
|
otherTeam = level.commonKeys.team1;
|
|
|
|
if ( sourceTeam == otherTeam )
|
|
{
|
|
otherTeam = level.commonKeys.team2;
|
|
}
|
|
|
|
teamPerformances = GetTeamPerformances();
|
|
players = level.players;
|
|
choice = undefined;
|
|
closest = 9999999;
|
|
|
|
for ( i = 0; i < players.size; i++ )
|
|
{
|
|
isExcluded = false;
|
|
|
|
for ( j = 0; j < excluded.size; j++ )
|
|
{
|
|
if ( excluded[j] == players[i] )
|
|
{
|
|
isExcluded = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( isExcluded )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]() != sourceTeam )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
clientPerformance = players[i] GetClientPerformanceOrDefault();
|
|
sourceTeamNewPerformance = teamPerformances[sourceTeam] - clientPerformance;
|
|
otherTeamNewPerformance = teamPerformances[otherTeam] + clientPerformance;
|
|
candidateValue = Abs( sourceTeamNewPerformance - otherTeamNewPerformance );
|
|
|
|
scripts\_integration_base::LogDebug( "perf=" + clientPerformance + " candidateValue=" + candidateValue + " src=" + sourceTeamNewPerformance + " dst=" + otherTeamNewPerformance );
|
|
|
|
if ( !IsDefined( choice ) )
|
|
{
|
|
choice = players[i];
|
|
closest = candidateValue;
|
|
}
|
|
|
|
else if ( candidateValue < closest )
|
|
{
|
|
scripts\_integration_base::LogDebug( candidateValue + " is the new best value ");
|
|
choice = players[i];
|
|
closest = candidateValue;
|
|
}
|
|
}
|
|
|
|
scripts\_integration_base::LogDebug( choice.name + " is the best candidate to swap" + " with closest=" + closest );
|
|
return choice;
|
|
}
|
|
|
|
GetTeamToJoin()
|
|
{
|
|
smallerTeam = GetSmallerTeam( 1 );
|
|
|
|
if ( IsDefined( smallerTeam ) )
|
|
{
|
|
return smallerTeam;
|
|
}
|
|
|
|
teamPerformances = GetTeamPerformances( self );
|
|
|
|
if ( teamPerformances[level.commonKeys.team1] < teamPerformances[level.commonKeys.team2] )
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team1 performance is lower, so selecting Team1" );
|
|
return level.commonKeys.team1;
|
|
}
|
|
|
|
else
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team2 performance is lower, so selecting Team2" );
|
|
return level.commonKeys.team2;
|
|
}
|
|
}
|
|
|
|
GetSmallerTeam( minDiff )
|
|
{
|
|
teamCounts = [[level.overrideMethods[level.commonFunctions.getTeamCounts]]]();
|
|
team1Count = teamCounts[level.commonKeys.team1];
|
|
team2Count = teamCounts[level.commonKeys.team2];
|
|
maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();
|
|
|
|
if ( team1Count == team2Count )
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
if ( team2Count == maxClients / 2 )
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team2 is full, so selecting Team1" );
|
|
return level.commonKeys.team1;
|
|
}
|
|
|
|
if ( team1Count == maxClients / 2 )
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team1 is full, so selecting Team2" );
|
|
return level.commonKeys.team2;
|
|
}
|
|
|
|
sizeDiscrepancy = Abs( team1Count - team2Count );
|
|
|
|
if ( sizeDiscrepancy > minDiff )
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team size differs by more than 1" );
|
|
|
|
if ( team1Count < team2Count )
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team1 is smaller, so selecting Team1" );
|
|
return level.commonKeys.team1;
|
|
}
|
|
|
|
else
|
|
{
|
|
scripts\_integration_base::LogDebug( "Team2 is smaller, so selecting Team2" );
|
|
return level.commonKeys.team2;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
GetTeamPerformances( ignoredClient )
|
|
{
|
|
players = level.players;
|
|
|
|
team1 = 0;
|
|
team2 = 0;
|
|
|
|
for ( i = 0; i < players.size; i++ )
|
|
{
|
|
if ( IsDefined( ignoredClient ) && players[i] == ignoredClient )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
performance = players[i] GetClientPerformanceOrDefault();
|
|
clientTeam = players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
|
|
|
|
if ( clientTeam == level.commonKeys.team1 )
|
|
{
|
|
team1 = team1 + performance;
|
|
}
|
|
else
|
|
{
|
|
team2 = team2 + performance;
|
|
}
|
|
}
|
|
|
|
result = [];
|
|
result[level.commonKeys.team1] = team1;
|
|
result[level.commonKeys.team2] = team2;
|
|
return result;
|
|
}
|
|
|
|
GetClientPerformanceOrDefault()
|
|
{
|
|
clientData = self.pers[level.clientDataKey];
|
|
performance = level.iw4madminIntegrationDefaultPerformance;
|
|
|
|
if ( IsDefined( clientData ) && IsDefined( clientData.performance ) )
|
|
{
|
|
performance = int( clientData.performance );
|
|
}
|
|
|
|
return performance;
|
|
}
|
|
|
|
GetPlayerFromClientNum( clientNum )
|
|
{
|
|
if ( clientNum < 0 )
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
for ( i = 0; i < level.players.size; i++ )
|
|
{
|
|
if ( level.players[i] getEntityNumber() == clientNum )
|
|
{
|
|
return level.players[i];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
OnPlayerJoinedTeam()
|
|
{
|
|
self endon( "disconnect" );
|
|
|
|
for( ;; )
|
|
{
|
|
self waittill( "joined_team" );
|
|
// join spec and join team occur at the same moment - out of order logging would be problematic
|
|
wait( 0.25 );
|
|
LogPrint( GenerateJoinTeamString( false ) );
|
|
}
|
|
}
|
|
|
|
OnPlayerJoinedSpectators()
|
|
{
|
|
self endon( "disconnect" );
|
|
|
|
for( ;; )
|
|
{
|
|
self waittill( "joined_spectators" );
|
|
LogPrint( GenerateJoinTeamString( true ) );
|
|
}
|
|
}
|
|
|
|
GenerateJoinTeamString( isSpectator )
|
|
{
|
|
team = self.team;
|
|
|
|
if ( IsDefined( self.joining_team ) )
|
|
{
|
|
team = self.joining_team;
|
|
}
|
|
else
|
|
{
|
|
if ( isSpectator || !IsDefined( team ) )
|
|
{
|
|
team = "spectator";
|
|
}
|
|
}
|
|
|
|
guid = self [[level.overrideMethods[level.commonFunctions.getXuid]]]();
|
|
|
|
if ( guid == "0" )
|
|
{
|
|
guid = self.guid;
|
|
}
|
|
|
|
if ( !IsDefined( guid ) || guid == "0" )
|
|
{
|
|
guid = "undefined";
|
|
}
|
|
|
|
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
|
|
}
|
|
|
|
PlayerTrackingOnInterval()
|
|
{
|
|
self endon( "disconnect" );
|
|
|
|
for ( ;; )
|
|
{
|
|
wait ( 120 );
|
|
if ( IsAlive( self ) )
|
|
{
|
|
self SaveTrackingMetrics();
|
|
}
|
|
}
|
|
}
|
|
|
|
SaveTrackingMetrics()
|
|
{
|
|
if ( !IsDefined( self.persistentClientId ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
scripts\_integration_base::LogDebug( "Saving tracking metrics for " + self.persistentClientId );
|
|
|
|
if ( !IsDefined( self.lastShotCount ) )
|
|
{
|
|
self.lastShotCount = 0;
|
|
}
|
|
|
|
currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
|
|
change = currentShotCount - self.lastShotCount;
|
|
self.lastShotCount = currentShotCount;
|
|
|
|
scripts\_integration_base::LogDebug( "Total Shots Fired increased by " + change );
|
|
|
|
if ( !IsDefined( change ) )
|
|
{
|
|
change = 0;
|
|
}
|
|
|
|
if ( change == 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
scripts\_integration_base::IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
|
|
} |