diff --git a/Application/Application.csproj b/Application/Application.csproj index efff2a19..400e4200 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -24,7 +24,7 @@ - + all diff --git a/Application/Extensions/ScriptPluginExtensions.cs b/Application/Extensions/ScriptPluginExtensions.cs index 6e03dd38..00c83584 100644 --- a/Application/Extensions/ScriptPluginExtensions.cs +++ b/Application/Extensions/ScriptPluginExtensions.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; +using Data.Models.Client.Stats; using Microsoft.EntityFrameworkCore; +using SharedLibraryCore; namespace IW4MAdmin.Application.Extensions; @@ -18,4 +20,15 @@ public static class ScriptPluginExtensions client.NetworkId }).ToList(); } + + public static IEnumerable GetClientsStatData(this DbSet set, int[] clientIds, + double serverId) + { + return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList(); + } + + public static object GetId(this Server server) + { + return server.GetIdForServer().GetAwaiter().GetResult(); + } } diff --git a/Data/Context/DatabaseContext.cs b/Data/Context/DatabaseContext.cs index 9e0bd9fb..fa65218a 100644 --- a/Data/Context/DatabaseContext.cs +++ b/Data/Context/DatabaseContext.cs @@ -32,6 +32,7 @@ namespace Data.Context public DbSet ClientMessages { get; set; } public DbSet ServerStatistics { get; set; } + public DbSet ClientStatistics { get; set; } public DbSet HitLocations { get; set; } public DbSet HitStatistics { get; set; } public DbSet Weapons { get; set; } diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 68c17586..ff17bc92 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -19,8 +19,10 @@ Setup() level.eventBus.timeoutKey = "timeout"; level.eventBus.timeout = 30; - level.commonFunctions = spawnstruct(); - level.commonFunctions.SetDvar = "SetDvarIfUninitialized"; + level.commonFunctions = spawnstruct(); + level.commonFunctions.setDvar = "SetDvarIfUninitialized"; + + level.commonKeys = spawnstruct(); level.notifyTypes = spawnstruct(); level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized"; @@ -116,17 +118,6 @@ OnPlayerSpawned() } } -OnPlayerDisconnect() -{ - self endon ( "disconnect" ); - - for ( ;; ) - { - self waittill( "disconnect" ); - self SaveTrackingMetrics(); - } -} - OnPlayerJoinedTeam() { self endon( "disconnect" ); @@ -245,7 +236,12 @@ _IsBot( entity ) _SetDvarIfUninitialized( dvarName, dvarValue ) { - [[level.overrideMethods[level.commonFunctions.SetDvar]]]( dvarName, dvarValue ); + [[level.overrideMethods[level.commonFunctions.setDvar]]]( dvarName, dvarValue ); +} + +NotImplementedFunction( a, b, c, d, e, f ) +{ + LogWarning( "Function not implemented" ); } // Not every game can output to console or even game log. @@ -675,6 +671,7 @@ OnClientDataReceived( event ) clientData.clientId = event.data["clientId"]; clientData.lastConnection = event.data["lastConnection"]; clientData.tag = event.data["tag"]; + clientData.performance = event.data["performance"]; clientData.state = "complete"; self.persistentClientId = event.data["clientId"]; diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index fec96461..ba09344c 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -16,9 +16,17 @@ Setup() scripts\_integration_base::RegisterLogger( ::Log2Console ); - level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired; - level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized; - level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout; + level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::_SetDvarIfUninitialized; + level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout; + level.overrideMethods[level.commonFunctions.changeTeam] = ::ChangeTeam; + level.overrideMethods[level.commonFunctions.getTeamCounts] = ::CountPlayers; + level.overrideMethods[level.commonFunctions.getMaxClients] = ::GetMaxClients; + level.overrideMethods[level.commonFunctions.getTeamBased] = ::GetTeamBased; + level.overrideMethods[level.commonFunctions.getClientTeam] = ::GetClientTeam; + level.overrideMethods[level.commonFunctions.getClientKillStreak] = ::GetClientKillStreak; + level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = ::BackupRestoreClientKillStreakData; + level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; RegisterClientCommands(); @@ -88,6 +96,88 @@ WaitForClientEvents() } } +GetMaxClients() +{ + return level.maxClients; +} + +GetTeamBased() +{ + return level.teamBased; +} + +CountPlayers() +{ + return maps\mp\gametypes\_teams::CountPlayers(); +} + +GetClientTeam() +{ + if ( IsDefined( self.pers["team"] ) && self.pers["team"] == "allies" ) + { + return "allies"; + } + + else if ( IsDefined( self.pers["team"] ) && self.pers["team"] == "axis" ) + { + return "axis"; + } + + else + { + return "none"; + } +} + +GetClientKillStreak() +{ + return int( self.pers["cur_kill_streak"] ); +} + +BackupRestoreClientKillStreakData( restore ) +{ + if ( restore ) + { + foreach ( index, streakStruct in self.pers["killstreaks_backup"] ) + { + self.pers["killstreaks"][index] = self.pers["killstreaks_backup"][index]; + } + } + + else + { + self.pers["killstreaks_backup"] = []; + + foreach ( index, streakStruct in self.pers["killstreaks"] ) + { + self.pers["killstreaks_backup"][index] = self.pers["killstreaks"][index]; + } + } +} + +WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 ) +{ + return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ); +} + +ChangeTeam( team ) +{ + switch ( team ) + { + case "allies": + self [[level.allies]](); + break; + + case "axis": + self [[level.axis]](); + break; + + case "spectator": + self [[level.spectator]](); + break; + } +} + GetTotalShotsFired() { return maps\mp\_utility::getPlayerStat( "mostshotsfired" ); diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc new file mode 100644 index 00000000..e61e3070 --- /dev/null +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -0,0 +1,466 @@ +Init() +{ + level thread Setup(); +} + +Setup() +{ + level endon( "game_ended" ); + + 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; + + // 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; + + if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) + { + return; + } + + if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 ) + { + return; + } + + level thread OnPlayerConnect(); +} + +OnPlayerConnect() +{ + level endon( level.eventTypes.gameEnd ); + + for ( ;; ) + { + level waittill( level.eventTypes.connect, player ); + + 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(); + player thread WaitForClientEvents(); + } +} + +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; +} + +WaitForClientEvents() +{ + self endon( level.eventTypes.disconnect ); + + for ( ;; ) + { + self waittill( level.eventTypes.localClientEvent, event ); + + if ( event.type == level.eventTypes.clientDataReceived ) + { + clientData = self.pers[level.clientDataKey]; + } + } +} diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index e72313ca..b82de5c4 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -77,6 +77,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterf ProjectSection(SolutionItems) = preProject GameFiles\GameInterface\_integration_base.gsc = GameFiles\GameInterface\_integration_base.gsc GameFiles\GameInterface\_integration_iw4x.gsc = GameFiles\GameInterface\_integration_iw4x.gsc + GameFiles\GameInterface\_integration_iw5.gsc = GameFiles\GameInterface\_integration_iw5.gsc + GameFiles\GameInterface\_integration_shared.gsc = GameFiles\GameInterface\_integration_shared.gsc + GameFiles\GameInterface\_integration_t5.gsc = GameFiles\GameInterface\_integration_t5.gsc EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}" diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 97c196f0..33a6764b 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -72,27 +72,27 @@ let plugin = { }; let commands = [{ - name: 'giveweapon', - description: 'gives specified weapon', - alias: 'gw', - permission: 'SeniorAdmin', - targetRequired: true, - arguments: [{ - name: 'player', - required: true - }, - { - name: 'weapon name', + name: 'giveweapon', + description: 'gives specified weapon', + alias: 'gw', + permission: 'SeniorAdmin', + targetRequired: true, + arguments: [{ + name: 'player', required: true - }], - supportedGames: ['IW4', 'IW5', 'T5'], - execute: (gameEvent) => { - if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { - return; + }, + { + name: 'weapon name', + required: true + }], + supportedGames: ['IW4', 'IW5', 'T5'], + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { + return; + } + sendScriptCommand(gameEvent.Owner, 'GiveWeapon', gameEvent.Origin, gameEvent.Target, {weaponName: gameEvent.Data}); } - sendScriptCommand(gameEvent.Owner, 'GiveWeapon', gameEvent.Origin, gameEvent.Target, {weaponName: gameEvent.Data}); - } -}, + }, { name: 'takeweapons', description: 'take all weapons from specified player', @@ -374,6 +374,15 @@ const initialize = (server) => { return true; } +const getClientStats = (client, server) => { + const contextFactory = _serviceResolver.ResolveService('IDatabaseContextFactory'); + const context = contextFactory.CreateContext(false); + const stats = context.ClientStatistics.GetClientsStatData([client.ClientId], server.GetId()); // .Find(client.ClientId, serverId); + context.Dispose(); + + return stats.length > 0 ? stats[0] : undefined; +} + function onReceivedDvar(server, dvarName, dvarValue, success) { const logger = _serviceResolver.ResolveService('ILogger'); logger.WriteDebug(`Received ${dvarName}=${dvarValue} success=${success}`); @@ -422,12 +431,14 @@ function onReceivedDvar(server, dvarName, dvarValue, success) { data[event.data] = meta === null ? '' : meta.Value; logger.WriteDebug(`event data is ${event.data}`); } else { + const clientStats = getClientStats(client, server); const tagMeta = metaService.GetPersistentMetaByLookup('ClientTagV2', 'ClientTagNameV2', client.ClientId, token).GetAwaiter().GetResult(); data = { level: client.Level, clientId: client.ClientId, lastConnection: client.LastConnection, - tag: tagMeta?.Value ?? '' + tag: tagMeta?.Value ?? '', + performance: clientStats?.Performance ?? 200.0 }; } @@ -456,13 +467,15 @@ function onReceivedDvar(server, dvarName, dvarValue, success) { } else { if (event.subType === 'Meta') { try { - logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`); - if (event.data['direction'] != null) { - event.data['direction'] = 'up' - ? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult() - : metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult(); - } else { - metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult(); + if (event.data['value'] != null && event.data['key'] != null) { + logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`); + if (event.data['direction'] != null) { + event.data['direction'] = 'up' + ? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult() + : metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult(); + } else { + metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult(); + } } sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Complete'}); } catch (error) { @@ -500,11 +513,13 @@ const pollForEvents = server => { } if (server.Throttled) { + logger.WriteDebug('Server is throttled so we are not polling for game data'); return; } if (!state.waitingOnInput) { state.waitingOnInput = true; + logger.WriteDebug('Attempting to get in dvar value'); getDvar(server, inDvar, onReceivedDvar); }