create shared integration for performance-based autobalance support

This commit is contained in:
RaidMax 2022-10-23 13:29:01 -05:00
parent 7c1c2e719b
commit 565f22b42e
8 changed files with 630 additions and 45 deletions

View File

@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2041" />
<PackageReference Include="Jint" Version="3.0.0-beta-2042" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>

View File

@ -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<object> GetClientsStatData(this DbSet<EFClientStatistics> 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();
}
}

View File

@ -32,6 +32,7 @@ namespace Data.Context
public DbSet<EFClientMessage> ClientMessages { get; set; }
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
public DbSet<EFClientStatistics> ClientStatistics { get; set; }
public DbSet<EFHitLocation> HitLocations { get; set; }
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
public DbSet<EFWeapon> Weapons { get; set; }

View File

@ -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"];

View File

@ -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" );

View File

@ -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];
}
}
}

View File

@ -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}"

View File

@ -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);
}