From ebdad2768dd92756704b4483253570cf2200cd5b Mon Sep 17 00:00:00 2001 From: RaidMax Date: Wed, 31 May 2023 11:28:51 -0500 Subject: [PATCH 01/19] fix plugin import debug log --- Application/Plugin/PluginImporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Application/Plugin/PluginImporter.cs b/Application/Plugin/PluginImporter.cs index 9df88890a..b3292096b 100644 --- a/Application/Plugin/PluginImporter.cs +++ b/Application/Plugin/PluginImporter.cs @@ -156,8 +156,8 @@ namespace IW4MAdmin.Application.Plugin } _logger.LogDebug("Discovered {Count} plugin implementations", pluginTypes.Count); - _logger.LogDebug("Discovered {Count} plugin commands", pluginTypes.Count); - _logger.LogDebug("Discovered {Count} configuration implementations", pluginTypes.Count); + _logger.LogDebug("Discovered {Count} plugin command implementations", commandTypes.Count); + _logger.LogDebug("Discovered {Count} plugin configuration implementations", configurationTypes.Count); return (pluginTypes, commandTypes, configurationTypes); } From 7323c6e3d7869696ef3d192b53d47b4d339aa4c3 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 1 Jun 2023 20:45:05 -0500 Subject: [PATCH 02/19] clean-up and make game interface gsc consistent --- GameFiles/GameInterface/_integration_base.gsc | 156 ++++------- GameFiles/GameInterface/_integration_iw4x.gsc | 47 ++-- GameFiles/GameInterface/_integration_iw5.gsc | 211 ++------------- .../GameInterface/_integration_shared.gsc | 247 ++++++++++-------- GameFiles/GameInterface/_integration_t5.gsc | 208 ++------------- GameFiles/GameInterface/_integration_t5zm.gsc | 220 ++-------------- GameFiles/GameInterface/_integration_t6.gsc | 228 ++-------------- .../_integration_t6zm_helper.gsc | 35 ++- 8 files changed, 316 insertions(+), 1036 deletions(-) diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index fac8a6688..3c4aa0c07 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -19,11 +19,15 @@ Setup() level.commonFunctions = spawnstruct(); level.commonFunctions.setDvar = "SetDvarIfUninitialized"; - level.commonFunctions.isBot = "IsBot"; - level.commonFunctions.getXuid = "GetXuid"; level.commonFunctions.getPlayerFromClientNum = "GetPlayerFromClientNum"; + level.commonFunctions.waittillNotifyOrTimeout = "WaittillNotifyOrTimeout"; + + level.overrideMethods = []; + level.overrideMethods[level.commonFunctions.setDvar] = scripts\_integration_base::NotImplementedFunction; + level.overrideMethods[level.commonFunctions.getPlayerFromClientNum] = ::_GetPlayerFromClientNum; level.commonKeys = spawnstruct(); + level.commonKeys.enabled = "sv_iw4madmin_integration_enabled"; level.notifyTypes = spawnstruct(); level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized"; @@ -51,12 +55,11 @@ Setup() level.clientCommandCallbacks = []; level.clientCommandRusAsTarget = []; level.logger = spawnstruct(); - level.overrideMethods = []; level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" ); InitializeLogger(); - wait ( 0.05 ); // needed to give script engine time to propagate notifies + wait ( 0.05 * 2 ); // needed to give script engine time to propagate notifies level notify( level.notifyTypes.integrationBootstrapInitialized ); level waittill( level.notifyTypes.gameFunctionsInitialized ); @@ -65,105 +68,26 @@ Setup() _SetDvarIfUninitialized( level.eventBus.inVar, "" ); _SetDvarIfUninitialized( level.eventBus.outVar, "" ); - _SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 ); + _SetDvarIfUninitialized( level.commonKeys.enabled, 1 ); _SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 ); - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) + if ( GetDvarInt( level.commonKeys.enabled) != 1 ) { return; } // start long running tasks - level thread MonitorClientEvents(); - level thread MonitorBus(); - level thread OnPlayerConnect(); + thread MonitorClientEvents(); + thread MonitorBus(); } ////////////////////////////////// // Client Methods ////////////////////////////////// -OnPlayerConnect() -{ - level endon ( "game_ended" ); - - for ( ;; ) - { - level waittill( "connected", player ); - - if ( _IsBot( player ) ) - { - // we don't want to track bots - continue; - } - - if ( !IsDefined( player.pers[level.clientDataKey] ) ) - { - player.pers[level.clientDataKey] = spawnstruct(); - } - - player thread OnPlayerSpawned(); - } -} - -OnPlayerSpawned() -{ - self endon( "disconnect" ); - - for ( ;; ) - { - self waittill( "spawned_player" ); - self PlayerSpawnEvents(); - } -} - -OnGameEnded() -{ - for ( ;; ) - { - level waittill( "game_ended" ); - // note: you can run data code here but it's possible for - // data to get truncated, so we will try a timer based approach for now - } -} - -DisplayWelcomeData() -{ - self endon( "disconnect" ); - - clientData = self.pers[level.clientDataKey]; - - if ( clientData.permissionLevel == "User" || clientData.permissionLevel == "Flagged" ) - { - return; - } - - self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel ); - wait( 2.0 ); - self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection ); -} - -PlayerSpawnEvents() -{ - self endon( "disconnect" ); - - clientData = self.pers[level.clientDataKey]; - - // this gives IW4MAdmin some time to register the player before making the request; - // although probably not necessary some users might have a slow database or poll rate - wait ( 2 ); - - if ( IsDefined( clientData.state ) && clientData.state == "complete" ) - { - return; - } - - self RequestClientBasicData(); -} - MonitorClientEvents() { - level endon( "game_ended" ); + level endon( level.eventTypes.gameEnd ); for ( ;; ) { @@ -178,6 +102,7 @@ MonitorClientEvents() client [[eventHandler]]( client.event ); LogDebug( "notify client for " + client.event.type ); client notify( level.eventTypes.localClientEvent, client.event ); + client notify( client.event.type, client.event ); } client.eventData = []; @@ -188,11 +113,13 @@ MonitorClientEvents() // Helper Methods ////////////////////////////////// -_IsBot( entity ) +NotImplementedFunction( a, b, c, d, e, f ) { - // there already is a cgame function exists as "IsBot", for IW4, but unsure what all titles have it defined, - // so we are defining it here - return IsDefined( entity.pers["isBot"] ) && entity.pers["isBot"]; + LogWarning( "Function not implemented" ); + if ( IsDefined ( a ) ) + { + LogWarning( a ); + } } _SetDvarIfUninitialized( dvarName, dvarValue ) @@ -200,9 +127,22 @@ _SetDvarIfUninitialized( dvarName, dvarValue ) [[level.overrideMethods[level.commonFunctions.setDvar]]]( dvarName, dvarValue ); } -NotImplementedFunction( a, b, c, d, e, f ) +_GetPlayerFromClientNum( clientNum ) { - LogWarning( "Function not implemented" ); + 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; } // Not every game can output to console or even game log. @@ -285,13 +225,13 @@ RegisterLogger( logger ) RequestClientMeta( metaKey ) { getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey ); - level thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self ); + thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self ); } RequestClientBasicData() { getClientDataEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "None", self, "" ); - level thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self ); + thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self ); } IncrementClientMeta( metaKey, incrementValue, clientId ) @@ -326,7 +266,7 @@ SetClientMeta( metaKey, metaValue, clientId, direction ) } setClientMetaEvent = BuildEventRequest( true, level.eventTypes.setClientDataRequested, "Meta", clientNumber, data ); - level thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self ); + thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self ); } BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) @@ -359,7 +299,7 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) MonitorBus() { - level endon( "game_ended" ); + level endon( level.eventTypes.gameEnd ); for( ;; ) { @@ -387,7 +327,7 @@ MonitorBus() QueueEvent( request, eventType, notifyEntity ) { - level endon( "game_ended" ); + level endon( level.eventTypes.gameEnd ); start = GetTime(); maxWait = level.eventBus.timeout * 1000; // 30 seconds @@ -395,7 +335,7 @@ QueueEvent( request, eventType, notifyEntity ) while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait ) { - level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 ); + level [[level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout]]]( "bus_ready", 1 ); if ( GetDvar( level.eventBus.inVar ) != "" ) { @@ -541,7 +481,7 @@ OnClientDataReceived( event ) metaKey = event.data[0]; clientData.meta[metaKey] = event.data[metaKey]; - LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] ); + LogDebug( "Meta Key=" + CoerceUndefined( metaKey ) + ", Meta Value=" + CoerceUndefined( event.data[metaKey] ) ); return; } @@ -553,8 +493,6 @@ OnClientDataReceived( event ) clientData.performance = event.data["performance"]; clientData.state = "complete"; self.persistentClientId = event.data["clientId"]; - - self thread DisplayWelcomeData(); } OnExecuteCommand( event ) @@ -590,5 +528,15 @@ OnExecuteCommand( event ) OnSetClientDataCompleted( event ) { // IW4MAdmin let us know it persisted (success or fail) - LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] ); + LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( event.data["status"] ) ); +} + +CoerceUndefined( object ) +{ + if ( !IsDefined( object ) ) + { + return "undefined"; + } + + return object; } diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index a072e7347..bfa1ce0b3 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -8,18 +8,17 @@ Init() Setup() { level endon( "game_ended" ); + waittillframeend; - // it's possible that the notify type has not been defined yet so we have to hard code it - level waittill( "SharedFunctionsInitialized" ); + level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "IW4"; scripts\_integration_base::RegisterLogger( ::Log2Console ); - level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired; - level.overrideMethods[level.commonFunctions.setDvar] = ::_SetDvarIfUninitialized; - level.overrideMethods[level.commonFunctions.isBot] = ::IsTestClient; - level.overrideMethods[level.commonFunctions.getXuid] = ::_GetXUID; - level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout; + level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper; + level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; + level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; level.overrideMethods[level.commonFunctions.changeTeam] = ::ChangeTeam; level.overrideMethods[level.commonFunctions.getTeamCounts] = ::CountPlayers; level.overrideMethods[level.commonFunctions.getMaxClients] = ::GetMaxClients; @@ -28,19 +27,18 @@ Setup() level.overrideMethods[level.commonFunctions.getClientKillStreak] = ::GetClientKillStreak; level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = ::BackupRestoreClientKillStreakData; level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; + level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; RegisterClientCommands(); - _SetDvarIfUninitialized( "sv_iw4madmin_autobalance", 0 ); - level notify( level.notifyTypes.gameFunctionsInitialized ); - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) + if ( GetDvarInt( level.commonKeys.enabled ) != 1 ) { return; } - level thread OnPlayerConnect(); + thread OnPlayerConnect(); } OnPlayerConnect() @@ -51,7 +49,7 @@ OnPlayerConnect() { level waittill( "connected", player ); - if ( player call [[ level.overrideMethods[ level.commonFunctions.isBot ] ]]() ) + if ( player IsTestClient() ) { // we don't want to track bots continue; @@ -186,12 +184,7 @@ GetTotalShotsFired() return maps\mp\_utility::getPlayerStat( "mostshotsfired" ); } -_SetDvarIfUninitialized( dvar, value ) -{ - SetDvarIfUninitialized( dvar, value ); -} - -_waittill_notify_or_timeout( _notify, timeout ) +WaitillNotifyOrTimeoutWrapper( _notify, timeout ) { common_scripts\utility::waittill_notify_or_timeout( _notify, timeout ); } @@ -201,11 +194,21 @@ Log2Console( logLevel, message ) PrintConsole( "[" + logLevel + "] " + message + "\n" ); } -_GetXUID() +SetDvarIfUninitializedWrapper( dvar, value ) +{ + SetDvarIfUninitialized( dvar, value ); +} + +GetXuidWrapper() { return self GetXUID(); } +IsBotWrapper( client ) +{ + return client IsTestClient(); +} + ////////////////////////////////// // GUID helpers ///////////////////////////////// @@ -519,11 +522,7 @@ HideImpl() AlertImpl( event, data ) { - if ( level.eventBus.gamename == "IW4" ) - { - self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 ); - } - + self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 ); return "Sent alert to " + self.name; } diff --git a/GameFiles/GameInterface/_integration_iw5.gsc b/GameFiles/GameInterface/_integration_iw5.gsc index 75057e375..16140fd84 100644 --- a/GameFiles/GameInterface/_integration_iw5.gsc +++ b/GameFiles/GameInterface/_integration_iw5.gsc @@ -8,50 +8,22 @@ Init() Setup() { level endon( "game_ended" ); + waittillframeend; - // it's possible that the notify type has not been defined yet so we have to hard code it - level waittill( "SharedFunctionsInitialized" ); + level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "IW5"; 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[level.commonFunctions.isBot] = ::IsTestClient; - level.overrideMethods[level.commonFunctions.getXuid] = ::_GetXUID; + level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper; + level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; + level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; + level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; RegisterClientCommands(); - _SetDvarIfUninitialized( "sv_iw4madmin_autobalance", 0 ); - level notify( level.notifyTypes.gameFunctionsInitialized ); - - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) - { - return; - } - - level thread OnPlayerConnect(); -} - -OnPlayerConnect() -{ - level endon ( "game_ended" ); - - for ( ;; ) - { - level waittill( "connected", player ); - - if ( player call [[ level.overrideMethods[ level.commonFunctions.isBot ] ]]() ) - { - // we don't want to track bots - continue; - } - - player thread SetPersistentData(); - player thread WaitForClientEvents(); - } } RegisterClientCommands() @@ -69,39 +41,17 @@ RegisterClientCommands() scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl ); } -WaitForClientEvents() -{ - self endon( "disconnect" ); - - // example of requesting a meta value - lastServerMetaKey = "LastServerPlayed"; - // self scripts\_integration_base::RequestClientMeta( lastServerMetaKey ); - - for ( ;; ) - { - self waittill( level.eventTypes.localClientEvent, event ); - - scripts\_integration_base::LogDebug( "Received client event " + event.type ); - - if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey ) - { - clientData = self.pers[level.clientDataKey]; - lastServerPlayed = clientData.meta[lastServerMetaKey]; - } - } -} - GetTotalShotsFired() { return maps\mp\_utility::getPlayerStat( "mostshotsfired" ); } -_SetDvarIfUninitialized( dvar, value ) +SetDvarIfUninitializedWrapper( dvar, value ) { SetDvarIfUninitialized( dvar, value ); } -_waittill_notify_or_timeout( _notify, timeout ) +WaitillNotifyOrTimeoutWrapper( _notify, timeout ) { common_scripts\utility::waittill_notify_or_timeout( _notify, timeout ); } @@ -111,142 +61,16 @@ Log2Console( logLevel, message ) Print( "[" + logLevel + "] " + message + "\n" ); } -_GetXUID() +IsBotWrapper( client ) +{ + return client IsTestClient(); +} + +GetXuidWrapper() { return self GetXUID(); } -////////////////////////////////// -// GUID helpers -///////////////////////////////// - -SetPersistentData() -{ - self endon( "disconnect" ); - - guidHigh = self GetPlayerData( "bests", "none" ); - guidLow = self GetPlayerData( "awards", "none" ); - persistentGuid = guidHigh + "," + guidLow; - guidIsStored = guidHigh != 0 && guidLow != 0; - - if ( guidIsStored ) - { - // give IW4MAdmin time to collect IP - wait( 15 ); - scripts\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid ); - scripts\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid ); - return; - } - - guid = self SplitGuid(); - - scripts\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow ); - - self SetPlayerData( "bests", "none", guid["high"] ); - self SetPlayerData( "awards", "none", guid["low"] ); -} - -SplitGuid() -{ - guid = self GetGuid(); - - if ( isDefined( self.guid ) ) - { - guid = self.guid; - } - - firstPart = 0; - secondPart = 0; - stringLength = 17; - firstPartExp = 0; - secondPartExp = 0; - - for ( i = stringLength - 1; i > 0; i-- ) - { - char = GetSubStr( guid, i - 1, i ); - if ( char == "" ) - { - char = "0"; - } - - if ( i > stringLength / 2 ) - { - value = GetIntForHexChar( char ); - power = Pow( 16, secondPartExp ); - secondPart = secondPart + ( value * power ); - secondPartExp++; - } - else - { - value = GetIntForHexChar( char ); - power = Pow( 16, firstPartExp ); - firstPart = firstPart + ( value * power ); - firstPartExp++; - } - } - - split = []; - split["low"] = int( secondPart ); - split["high"] = int( firstPart ); - - return split; -} - -Pow( num, exponent ) -{ - result = 1; - while( exponent != 0 ) - { - result = result * num; - exponent--; - } - - return result; -} - -GetIntForHexChar( char ) -{ - char = ToLower( char ); - // generated by co-pilot because I can't be bothered to make it more "elegant" - switch( char ) - { - case "0": - return 0; - case "1": - return 1; - case "2": - return 2; - case "3": - return 3; - case "4": - return 4; - case "5": - return 5; - case "6": - return 6; - case "7": - return 7; - case "8": - return 8; - case "9": - return 9; - case "a": - return 10; - case "b": - return 11; - case "c": - return 12; - case "d": - return 13; - case "e": - return 14; - case "f": - return 15; - default: - return 0; - } -} - ////////////////////////////////// // Command Implementations ///////////////////////////////// @@ -427,10 +251,7 @@ HideImpl() AlertImpl( event, data ) { - if ( level.eventBus.gamename == "IW5" ) { - self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 ); - } - + self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 ); return "Sent alert to " + self.name; } diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index 2c0d87bd1..aa517b6b8 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -1,4 +1,3 @@ - Init() { thread Setup(); @@ -6,10 +5,10 @@ Init() Setup() { + wait ( 0.05 ); 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 waittill( level.notifyTypes.integrationBootstrapInitialized ); level.commonFunctions.changeTeam = "ChangeTeam"; level.commonFunctions.getTeamCounts = "GetTeamCounts"; @@ -18,7 +17,10 @@ Setup() level.commonFunctions.getClientTeam = "GetClientTeam"; level.commonFunctions.getClientKillStreak = "GetClientKillStreak"; level.commonFunctions.backupRestoreClientKillStreakData = "BackupRestoreClientKillStreakData"; + level.commonFunctions.getTotalShotsFired = "GetTotalShotsFired"; level.commonFunctions.waitTillAnyTimeout = "WaitTillAnyTimeout"; + level.commonFunctions.isBot = "IsBot"; + level.commonFunctions.getXuid = "GetXuid"; level.overrideMethods[level.commonFunctions.changeTeam] = scripts\_integration_base::NotImplementedFunction; level.overrideMethods[level.commonFunctions.getTeamCounts] = scripts\_integration_base::NotImplementedFunction; @@ -28,16 +30,19 @@ Setup() 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; + level.overrideMethods[level.commonFunctions.getXuid] = scripts\_integration_base::NotImplementedFunction; + level.overrideMethods[level.commonFunctions.isBot] = 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.commonKeys.autoBalance = "sv_iw4madmin_autobalance"; level.eventTypes.connect = "connected"; level.eventTypes.disconnect = "disconnect"; level.eventTypes.joinTeam = "joined_team"; + level.eventTypes.joinSpec = "joined_spectators"; level.eventTypes.spawned = "spawned_player"; level.eventTypes.gameEnd = "game_ended"; @@ -45,13 +50,20 @@ Setup() level notify( level.notifyTypes.sharedFunctionsInitialized ); level waittill( level.notifyTypes.gameFunctionsInitialized ); - - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) + + scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.autoBalance, 0 ); + + if ( GetDvarInt( level.commonKeys.enabled ) != 1 ) { return; } - level thread OnPlayerConnect(); + thread OnPlayerConnect(); +} + +_IsBot( player ) +{ + return [[level.overrideMethods[level.commonFunctions.isBot]]]( player ); } OnPlayerConnect() @@ -62,17 +74,23 @@ OnPlayerConnect() { level waittill( level.eventTypes.connect, player ); - if ( scripts\_integration_base::_IsBot( player ) ) + if ( _IsBot( player ) ) { // we don't want to track bots continue; } + + if ( !IsDefined( player.pers[level.clientDataKey] ) ) + { + player.pers[level.clientDataKey] = spawnstruct(); + } + player thread OnPlayerSpawned(); player thread OnPlayerJoinedTeam(); player thread OnPlayerJoinedSpectators(); player thread PlayerTrackingOnInterval(); - if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 || !IsDefined( [[level.overrideMethods[level.commonFunctions.getTeamBased]]]() ) ) + if ( GetDvarInt( level.commonKeys.autoBalance ) != 1 || !IsDefined( [[level.overrideMethods[level.commonFunctions.getTeamBased]]]() ) ) { continue; } @@ -85,13 +103,91 @@ OnPlayerConnect() teamToJoin = player GetTeamToJoin(); player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin ); - player thread OnClientFirstSpawn(); - player thread OnClientJoinedTeam(); - player thread OnClientDisconnect(); + player thread OnPlayerFirstSpawn(); + player thread OnPlayerDisconnect(); } } -OnClientDisconnect() +PlayerSpawnEvents() +{ + self endon( level.eventTypes.disconnect ); + + clientData = self.pers[level.clientDataKey]; + + // this gives IW4MAdmin some time to register the player before making the request; + // although probably not necessary some users might have a slow database or poll rate + wait ( 2 ); + + if ( IsDefined( clientData.state ) && clientData.state == "complete" ) + { + return; + } + + self scripts\_integration_base::RequestClientBasicData(); + + self waittill( level.eventTypes.clientDataReceived, clientEvent ); + + if ( clientData.permissionLevel == "User" || clientData.permissionLevel == "Flagged" ) + { + return; + } + + self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel ); + wait( 2.0 ); + self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection ); +} + + +PlayerTrackingOnInterval() +{ + self endon( level.eventTypes.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 ); +} + +// #region team balance + +OnPlayerDisconnect() { level endon( level.eventTypes.gameEnd ); self endon( "disconnect_logic_end" ); @@ -106,7 +202,7 @@ OnClientDisconnect() } } -OnClientJoinedTeam() +OnPlayerJoinedTeam() { self endon( level.eventTypes.disconnect ); @@ -114,6 +210,14 @@ OnClientJoinedTeam() { self waittill( level.eventTypes.joinTeam ); + wait( 0.25 ); + LogPrint( GenerateJoinTeamString( false ) ); + + if ( GetDvarInt( level.commonKeys.autoBalance ) != 1 ) + { + continue; + } + if ( IsDefined( self.wasAutoBalanced ) && self.wasAutoBalanced ) { self.wasAutoBalanced = false; @@ -141,12 +245,34 @@ OnClientJoinedTeam() } } -OnClientFirstSpawn() +OnPlayerSpawned() +{ + self endon( level.eventTypes.disconnect ); + + for ( ;; ) + { + self waittill( level.eventTypes.spawned ); + self thread PlayerSpawnEvents(); + } +} + +OnPlayerJoinedSpectators() +{ + self endon( level.eventTypes.disconnect ); + + for( ;; ) + { + self waittill( level.eventTypes.joinSpec ); + LogPrint( GenerateJoinTeamString( true ) ); + } +} + +OnPlayerFirstSpawn() { self endon( level.eventTypes.disconnect ); timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.spawned ); - if ( timeoutResult != "timeout" ) + if ( timeoutResult != level.eventBus.timeoutKey ) { return; } @@ -467,48 +593,6 @@ GetClientPerformanceOrDefault() 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; @@ -540,49 +624,4 @@ GenerateJoinTeamString( isSpectator ) 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 ); -} \ No newline at end of file +// #end region diff --git a/GameFiles/GameInterface/_integration_t5.gsc b/GameFiles/GameInterface/_integration_t5.gsc index 139847179..962da312e 100644 --- a/GameFiles/GameInterface/_integration_t5.gsc +++ b/GameFiles/GameInterface/_integration_t5.gsc @@ -8,49 +8,22 @@ Init() Setup() { level endon( "game_ended" ); + waittillframeend; - // it's possible that the notify type has not been defined yet so we have to hard code it - level waittill( "SharedFunctionsInitialized" ); + level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "T5"; 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[level.commonFunctions.getXuid] = ::_GetXUID; + level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper; + level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; + level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; + level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; RegisterClientCommands(); - _SetDvarIfUninitialized( "sv_iw4madmin_autobalance", 0 ); - level notify( level.notifyTypes.gameFunctionsInitialized ); - - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) - { - return; - } - - level thread OnPlayerConnect(); -} - -OnPlayerConnect() -{ - level endon ( "game_ended" ); - - for ( ;; ) - { - level waittill( "connected", player ); - - if ( scripts\_integration_base::_IsBot( player ) ) - { - // we don't want to track bots - continue; - } - - //player thread SetPersistentData(); - player thread WaitForClientEvents(); - } } RegisterClientCommands() @@ -68,39 +41,17 @@ RegisterClientCommands() scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl ); } -WaitForClientEvents() -{ - self endon( "disconnect" ); - - // example of requesting a meta value - lastServerMetaKey = "LastServerPlayed"; - // self scripts\_integration_base::RequestClientMeta( lastServerMetaKey ); - - for ( ;; ) - { - self waittill( level.eventTypes.localClientEvent, event ); - - scripts\_integration_base::LogDebug( "Received client event " + event.type ); - - if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey ) - { - clientData = self.pers[level.clientDataKey]; - lastServerPlayed = clientData.meta[lastServerMetaKey]; - } - } -} - GetTotalShotsFired() { return maps\mp\gametypes\_persistence::statGet( "total_shots" ); } -_SetDvarIfUninitialized(dvar, value) +SetDvarIfUninitializedWrapper( dvar, value ) { - maps\mp\_utility::set_dvar_if_unset(dvar, value); + maps\mp\_utility::set_dvar_if_unset( dvar, value ); } -_waittill_notify_or_timeout( msg, timer ) +WaitillNotifyOrTimeoutWrapper( msg, timer ) { self endon( msg ); wait( timer ); @@ -113,7 +64,6 @@ Log2Console( logLevel, message ) God() { - if ( !IsDefined( self.godmode ) ) { self.godmode = false; @@ -131,142 +81,16 @@ God() } } -_GetXUID() +IsBotWrapper( client ) +{ + return client maps\mp\_utility::is_bot(); +} + +GetXuidWrapper() { return self GetXUID(); } -////////////////////////////////// -// GUID helpers -///////////////////////////////// - -/*SetPersistentData() -{ - self endon( "disconnect" ); - - guidHigh = self GetPlayerData( "bests", "none" ); - guidLow = self GetPlayerData( "awards", "none" ); - persistentGuid = guidHigh + "," + guidLow; - guidIsStored = guidHigh != 0 && guidLow != 0; - - if ( guidIsStored ) - { - // give IW4MAdmin time to collect IP - wait( 15 ); - scripts\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid ); - scripts\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid ); - return; - } - - guid = self SplitGuid(); - - scripts\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow ); - - self SetPlayerData( "bests", "none", guid["high"] ); - self SetPlayerData( "awards", "none", guid["low"] ); -} - -SplitGuid() -{ - guid = self GetGuid(); - - if ( isDefined( self.guid ) ) - { - guid = self.guid; - } - - firstPart = 0; - secondPart = 0; - stringLength = 17; - firstPartExp = 0; - secondPartExp = 0; - - for ( i = stringLength - 1; i > 0; i-- ) - { - char = GetSubStr( guid, i - 1, i ); - if ( char == "" ) - { - char = "0"; - } - - if ( i > stringLength / 2 ) - { - value = GetIntForHexChar( char ); - power = Pow( 16, secondPartExp ); - secondPart = secondPart + ( value * power ); - secondPartExp++; - } - else - { - value = GetIntForHexChar( char ); - power = Pow( 16, firstPartExp ); - firstPart = firstPart + ( value * power ); - firstPartExp++; - } - } - - split = []; - split["low"] = int( secondPart ); - split["high"] = int( firstPart ); - - return split; -} - -Pow( num, exponent ) -{ - result = 1; - while( exponent != 0 ) - { - result = result * num; - exponent--; - } - - return result; -} - -GetIntForHexChar( char ) -{ - char = ToLower( char ); - // generated by co-pilot because I can't be bothered to make it more "elegant" - switch( char ) - { - case "0": - return 0; - case "1": - return 1; - case "2": - return 2; - case "3": - return 3; - case "4": - return 4; - case "5": - return 5; - case "6": - return 6; - case "7": - return 7; - case "8": - return 8; - case "9": - return 9; - case "a": - return 10; - case "b": - return 11; - case "c": - return 12; - case "d": - return 13; - case "e": - return 14; - case "f": - return 15; - default: - return 0; - } -}*/ - ////////////////////////////////// // Command Implementations ///////////////////////////////// diff --git a/GameFiles/GameInterface/_integration_t5zm.gsc b/GameFiles/GameInterface/_integration_t5zm.gsc index d01e9dc28..e7d461e96 100644 --- a/GameFiles/GameInterface/_integration_t5zm.gsc +++ b/GameFiles/GameInterface/_integration_t5zm.gsc @@ -9,48 +9,21 @@ 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( "SharedFunctionsInitialized" ); + level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "T5"; 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["GetPlayerFromClientNum"] = ::_GetPlayerFromClientNum; + level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper; + level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; + level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; + level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; + level.overrideMethods[level.commonFunction.getPlayerFromClientNum] = ::_GetPlayerFromClientNum; RegisterClientCommands(); - _SetDvarIfUninitialized( "sv_iw4madmin_autobalance", 0 ); - level notify( level.notifyTypes.gameFunctionsInitialized ); - - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) - { - return; - } - - level thread OnPlayerConnect(); -} - -OnPlayerConnect() -{ - level endon ( "game_ended" ); - - for ( ;; ) - { - level waittill( "connected", player ); - - if ( scripts\_integration_base::_IsBot( player ) ) - { - // we don't want to track bots - continue; - } - - //player thread SetPersistentData(); - player thread WaitForClientEvents(); - } } RegisterClientCommands() @@ -68,45 +41,23 @@ RegisterClientCommands() scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl ); } -WaitForClientEvents() -{ - self endon( "disconnect" ); - - // example of requesting a meta value - lastServerMetaKey = "LastServerPlayed"; - // self scripts\_integration_base::RequestClientMeta( lastServerMetaKey ); - - for ( ;; ) - { - self waittill( level.eventTypes.localClientEvent, event ); - - scripts\_integration_base::LogDebug( "Received client event " + event.type ); - - if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey ) - { - clientData = self.pers[level.clientDataKey]; - lastServerPlayed = clientData.meta[lastServerMetaKey]; - } - } -} - GetTotalShotsFired() { return 0; //ZM has no shot tracking. TODO: add tracking function for event weapon_fired } -_SetDvarIfUninitialized(dvar, value) +SetDvarIfUninitializedWrapper( dvar, value ) { - if (GetDvar(dvar)=="" ) + if ( GetDvar( dvar ) == "" ) { - SetDvar(dvar, value); + SetDvar( dvar, value ); return value; } - return GetDvar(dvar); + return GetDvar( dvar ); } -_waittill_notify_or_timeout( msg, timer ) +WaitillNotifyOrTimeoutWrapper( msg, timer ) { self endon( msg ); wait( timer ); @@ -119,7 +70,6 @@ Log2Console( logLevel, message ) God() { - if ( !IsDefined( self.godmode ) ) { self.godmode = false; @@ -137,6 +87,16 @@ God() } } +IsBotWrapper( client ) +{ + return ( IsDefined ( client.pers["isBot"] ) && client.pers["isBot"] != 0 ); +} + +GetXuidWrapper() +{ + return self GetXUID(); +} + _GetPlayerFromClientNum( clientNum ) { if ( clientNum < 0 ) @@ -148,7 +108,8 @@ _GetPlayerFromClientNum( clientNum ) for ( i = 0; i < players.size; i++ ) { - scripts\_integration_base::LogDebug(i+"/"+players.size+ "=" + players[i].name); + scripts\_integration_base::LogDebug( i+"/"+players.size+ "=" + players[i].name ); + if ( players[i] getEntityNumber() == clientNum ) { return players[i]; @@ -158,137 +119,6 @@ _GetPlayerFromClientNum( clientNum ) return undefined; } -////////////////////////////////// -// GUID helpers -///////////////////////////////// - -/*SetPersistentData() -{ - self endon( "disconnect" ); - - guidHigh = self GetPlayerData( "bests", "none" ); - guidLow = self GetPlayerData( "awards", "none" ); - persistentGuid = guidHigh + "," + guidLow; - guidIsStored = guidHigh != 0 && guidLow != 0; - - if ( guidIsStored ) - { - // give IW4MAdmin time to collect IP - wait( 15 ); - scripts\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid ); - scripts\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid ); - return; - } - - guid = self SplitGuid(); - - scripts\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow ); - - self SetPlayerData( "bests", "none", guid["high"] ); - self SetPlayerData( "awards", "none", guid["low"] ); -} - -SplitGuid() -{ - guid = self GetGuid(); - - if ( isDefined( self.guid ) ) - { - guid = self.guid; - } - - firstPart = 0; - secondPart = 0; - stringLength = 17; - firstPartExp = 0; - secondPartExp = 0; - - for ( i = stringLength - 1; i > 0; i-- ) - { - char = GetSubStr( guid, i - 1, i ); - if ( char == "" ) - { - char = "0"; - } - - if ( i > stringLength / 2 ) - { - value = GetIntForHexChar( char ); - power = Pow( 16, secondPartExp ); - secondPart = secondPart + ( value * power ); - secondPartExp++; - } - else - { - value = GetIntForHexChar( char ); - power = Pow( 16, firstPartExp ); - firstPart = firstPart + ( value * power ); - firstPartExp++; - } - } - - split = []; - split["low"] = int( secondPart ); - split["high"] = int( firstPart ); - - return split; -} - -Pow( num, exponent ) -{ - result = 1; - while( exponent != 0 ) - { - result = result * num; - exponent--; - } - - return result; -} - -GetIntForHexChar( char ) -{ - char = ToLower( char ); - // generated by co-pilot because I can't be bothered to make it more "elegant" - switch( char ) - { - case "0": - return 0; - case "1": - return 1; - case "2": - return 2; - case "3": - return 3; - case "4": - return 4; - case "5": - return 5; - case "6": - return 6; - case "7": - return 7; - case "8": - return 8; - case "9": - return 9; - case "a": - return 10; - case "b": - return 11; - case "c": - return 12; - case "d": - return 13; - case "e": - return 14; - case "f": - return 15; - default: - return 0; - } -}*/ - ////////////////////////////////// // Command Implementations ///////////////////////////////// @@ -472,7 +302,7 @@ HideImpl( event, data ) AlertImpl( event, data ) { //self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 ); - self IPrintLnBold(data["message"]); + self IPrintLnBold( data["message"] ); return "Sent alert to " + self.name; } diff --git a/GameFiles/GameInterface/_integration_t6.gsc b/GameFiles/GameInterface/_integration_t6.gsc index 07b67b649..3c5d578b1 100644 --- a/GameFiles/GameInterface/_integration_t6.gsc +++ b/GameFiles/GameInterface/_integration_t6.gsc @@ -9,49 +9,22 @@ Init() Setup() { level endon( "game_ended" ); + waittillframeend; - // it's possible that the notify type has not been defined yet so we have to hard code it - level waittill( "SharedFunctionsInitialized" ); + level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "T6"; 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[level.commonFunctions.getXuid] = ::_GetXUID; + level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper; + level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; + level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; + level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; RegisterClientCommands(); - - _SetDvarIfUninitialized( "sv_iw4madmin_autobalance", 0 ); - + level notify( level.notifyTypes.gameFunctionsInitialized ); - - if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) - { - return; - } - - level thread OnPlayerConnect(); -} - -OnPlayerConnect() -{ - level endon ( "game_ended" ); - - for ( ;; ) - { - level waittill( "connected", player ); - - if ( scripts\_integration_base::_IsBot( player ) ) - { - // we don't want to track bots - continue; - } - - //player thread SetPersistentData(); - player thread WaitForClientEvents(); - } } RegisterClientCommands() @@ -69,39 +42,17 @@ RegisterClientCommands() scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl ); } -WaitForClientEvents() -{ - self endon( "disconnect" ); - - // example of requesting a meta value - lastServerMetaKey = "LastServerPlayed"; - // self scripts\_integration_base::RequestClientMeta( lastServerMetaKey ); - - for ( ;; ) - { - self waittill( level.eventTypes.localClientEvent, event ); - - scripts\_integration_base::LogDebug( "Received client event " + event.type ); - - if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey ) - { - clientData = self.pers[level.clientDataKey]; - lastServerPlayed = clientData.meta[lastServerMetaKey]; - } - } -} - GetTotalShotsFired() { - return self.pers[ "total_shots" ]; + return self.pers["total_shots"]; } -_SetDvarIfUninitialized(dvar, value) +SetDvarIfUninitializedWrapper( dvar, value ) { - maps\mp\_utility::set_dvar_if_unset(dvar, value); + maps\mp\_utility::set_dvar_if_unset( dvar, value ); } -_waittill_notify_or_timeout( msg, timer ) +WaitillNotifyOrTimeoutWrapper( msg, timer ) { self endon( msg ); wait( timer ); @@ -114,7 +65,6 @@ Log2Console( logLevel, message ) God() { - if ( !IsDefined( self.godmode ) ) { self.godmode = false; @@ -132,142 +82,16 @@ God() } } -_GetXUID() +IsBotWrapper( client ) +{ + return client maps\mp\_utility::is_bot(); +} + +GetXuidWrapper() { return self GetXUID(); } -////////////////////////////////// -// GUID helpers -///////////////////////////////// - -/*SetPersistentData() -{ - self endon( "disconnect" ); - - guidHigh = self GetPlayerData( "bests", "none" ); - guidLow = self GetPlayerData( "awards", "none" ); - persistentGuid = guidHigh + "," + guidLow; - guidIsStored = guidHigh != 0 && guidLow != 0; - - if ( guidIsStored ) - { - // give IW4MAdmin time to collect IP - wait( 15 ); - scripts\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid ); - scripts\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid ); - return; - } - - guid = self SplitGuid(); - - scripts\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow ); - - self SetPlayerData( "bests", "none", guid["high"] ); - self SetPlayerData( "awards", "none", guid["low"] ); -} - -SplitGuid() -{ - guid = self GetGuid(); - - if ( isDefined( self.guid ) ) - { - guid = self.guid; - } - - firstPart = 0; - secondPart = 0; - stringLength = 17; - firstPartExp = 0; - secondPartExp = 0; - - for ( i = stringLength - 1; i > 0; i-- ) - { - char = GetSubStr( guid, i - 1, i ); - if ( char == "" ) - { - char = "0"; - } - - if ( i > stringLength / 2 ) - { - value = GetIntForHexChar( char ); - power = Pow( 16, secondPartExp ); - secondPart = secondPart + ( value * power ); - secondPartExp++; - } - else - { - value = GetIntForHexChar( char ); - power = Pow( 16, firstPartExp ); - firstPart = firstPart + ( value * power ); - firstPartExp++; - } - } - - split = []; - split["low"] = int( secondPart ); - split["high"] = int( firstPart ); - - return split; -} - -Pow( num, exponent ) -{ - result = 1; - while( exponent != 0 ) - { - result = result * num; - exponent--; - } - - return result; -} - -GetIntForHexChar( char ) -{ - char = ToLower( char ); - // generated by co-pilot because I can't be bothered to make it more "elegant" - switch( char ) - { - case "0": - return 0; - case "1": - return 1; - case "2": - return 2; - case "3": - return 3; - case "4": - return 4; - case "5": - return 5; - case "6": - return 6; - case "7": - return 7; - case "8": - return 8; - case "9": - return 9; - case "a": - return 10; - case "b": - return 11; - case "c": - return 12; - case "d": - return 13; - case "e": - return 14; - case "f": - return 15; - default: - return 0; - } -}*/ - ////////////////////////////////// // Command Implementations ///////////////////////////////// @@ -456,17 +280,7 @@ HideImpl( event, data ) AlertImpl( event, data ) { - /*if ( !sessionmodeiszombiesgame() ) - {*/ - self thread oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 ); - /*} - else - { - self IPrintLnBold( data["alertType"] ); - self IPrintLnBold( data["message"] ); - }*/ - - + self thread oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 ); return "Sent alert to " + self.name; } @@ -550,7 +364,7 @@ SetSpectatorImpl( event, data ) ///////////////////////////////// /* -1:1 the same on MP and ZM but in different includes. Since we probably want to be able to send Alerts on non teambased wagermatechs use our own copy. +1:1 the same on MP and ZM but in different includes. Since we probably want to be able to send Alerts on non teambased wagermatches use our own copy. */ oldnotifymessage( titletext, notifytext, iconname, glowcolor, sound, duration ) { @@ -567,5 +381,3 @@ oldnotifymessage( titletext, notifytext, iconname, glowcolor, sound, duration ) self.startmessagenotifyqueue[ self.startmessagenotifyqueue.size ] = notifydata; self notify( "received award" ); } - - diff --git a/GameFiles/GameInterface/_integration_t6zm_helper.gsc b/GameFiles/GameInterface/_integration_t6zm_helper.gsc index 69a26dbda..22ea47eec 100644 --- a/GameFiles/GameInterface/_integration_t6zm_helper.gsc +++ b/GameFiles/GameInterface/_integration_t6zm_helper.gsc @@ -1,45 +1,48 @@ -init() +Init() { - level.startmessagedefaultduration = 2; level.regulargamemessages = spawnstruct(); level.regulargamemessages.waittime = 6; - - level thread onplayerconnect(); + thread OnPlayerConnect(); } -onplayerconnect() +OnPlayerConnect() { for ( ;; ) { level waittill( "connecting", player ); - player thread displaypopupswaiter(); + player thread DisplaypopupsWaiter()(); } } -displaypopupswaiter() +DisplaypopupsWaiter() { self endon( "disconnect" ); self.ranknotifyqueue = []; - if ( !isDefined( self.pers[ "challengeNotifyQueue" ] ) ) + + if ( !IsDefined( self.pers[ "challengeNotifyQueue" ] ) ) { self.pers[ "challengeNotifyQueue" ] = []; } - if ( !isDefined( self.pers[ "contractNotifyQueue" ] ) ) + if ( !IsDefined( self.pers[ "contractNotifyQueue" ] ) ) { self.pers[ "contractNotifyQueue" ] = []; } + self.messagenotifyqueue = []; self.startmessagenotifyqueue = []; self.wagernotifyqueue = []; + while ( !level.gameended ) { if ( self.startmessagenotifyqueue.size == 0 && self.messagenotifyqueue.size == 0 ) { self waittill( "received award" ); } + waittillframeend; + if ( level.gameended ) { return; @@ -50,7 +53,7 @@ displaypopupswaiter() { nextnotifydata = self.startmessagenotifyqueue[ 0 ]; arrayremoveindex( self.startmessagenotifyqueue, 0, 0 ); - if ( isDefined( nextnotifydata.duration ) ) + if ( IsDefined( nextnotifydata.duration ) ) { duration = nextnotifydata.duration; } @@ -58,15 +61,18 @@ displaypopupswaiter() { duration = level.startmessagedefaultduration; } + self maps\mp\gametypes_zm\_hud_message::shownotifymessage( nextnotifydata, duration ); - wait duration; + wait ( duration ); + continue; } else if ( self.messagenotifyqueue.size > 0 ) { nextnotifydata = self.messagenotifyqueue[ 0 ]; arrayremoveindex( self.messagenotifyqueue, 0, 0 ); - if ( isDefined( nextnotifydata.duration ) ) + + if ( IsDefined( nextnotifydata.duration ) ) { duration = nextnotifydata.duration; } @@ -74,13 +80,14 @@ displaypopupswaiter() { duration = level.regulargamemessages.waittime; } + self maps\mp\gametypes_zm\_hud_message::shownotifymessage( nextnotifydata, duration ); continue; } else { - wait 1; + wait ( 1 ); } } } -} \ No newline at end of file +} From bc34211e438823c3b18077583a0f9e649a3bcd43 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 1 Jun 2023 21:09:18 -0500 Subject: [PATCH 03/19] more game interface gsc tweaks --- GameFiles/GameInterface/_integration_base.gsc | 2 +- GameFiles/GameInterface/_integration_iw4x.gsc | 2 +- GameFiles/GameInterface/_integration_shared.gsc | 2 +- GameFiles/GameInterface/_integration_t5zm.gsc | 2 ++ GameFiles/GameInterface/_integration_t6.gsc | 6 ++++++ 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 3c4aa0c07..6ff020844 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -71,7 +71,7 @@ Setup() _SetDvarIfUninitialized( level.commonKeys.enabled, 1 ); _SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 ); - if ( GetDvarInt( level.commonKeys.enabled) != 1 ) + if ( GetDvarInt( level.commonKeys.enabled ) != 1 ) { return; } diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index bfa1ce0b3..71f9454ef 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -10,7 +10,7 @@ Setup() level endon( "game_ended" ); waittillframeend; - level waittill( level.notifyTypes.sharedFunctionsInitialized ); + level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "IW4"; scripts\_integration_base::RegisterLogger( ::Log2Console ); diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index aa517b6b8..be81e2daa 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -230,7 +230,7 @@ OnPlayerJoinedTeam() if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 ) { OnTeamSizeChanged(); - scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" ); + scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" ); continue; } diff --git a/GameFiles/GameInterface/_integration_t5zm.gsc b/GameFiles/GameInterface/_integration_t5zm.gsc index e7d461e96..9a69d855a 100644 --- a/GameFiles/GameInterface/_integration_t5zm.gsc +++ b/GameFiles/GameInterface/_integration_t5zm.gsc @@ -8,9 +8,11 @@ Init() Setup() { level endon( "game_ended" ); + waittillframeend; level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "T5"; + level.eventTypes.gameEnd = "end_game"; scripts\_integration_base::RegisterLogger( ::Log2Console ); diff --git a/GameFiles/GameInterface/_integration_t6.gsc b/GameFiles/GameInterface/_integration_t6.gsc index 3c5d578b1..18323bf83 100644 --- a/GameFiles/GameInterface/_integration_t6.gsc +++ b/GameFiles/GameInterface/_integration_t6.gsc @@ -9,10 +9,16 @@ Init() Setup() { level endon( "game_ended" ); + level endon( "end_game" ); waittillframeend; level waittill( level.notifyTypes.sharedFunctionsInitialized ); level.eventBus.gamename = "T6"; + + if ( sessionmodeiszombiesgame() ) + { + level.eventTypes.gameEnd = "end_game"; + } scripts\_integration_base::RegisterLogger( ::Log2Console ); From b4f93602ef9222d0fd7387c12e2b473eff32ad42 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 1 Jun 2023 21:11:08 -0500 Subject: [PATCH 04/19] update t5zm game interface gsc game end event --- GameFiles/GameInterface/_integration_t5zm.gsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GameFiles/GameInterface/_integration_t5zm.gsc b/GameFiles/GameInterface/_integration_t5zm.gsc index 9a69d855a..0b2efd8cc 100644 --- a/GameFiles/GameInterface/_integration_t5zm.gsc +++ b/GameFiles/GameInterface/_integration_t5zm.gsc @@ -7,7 +7,7 @@ Init() Setup() { - level endon( "game_ended" ); + level endon( "end_game" ); waittillframeend; level waittill( level.notifyTypes.sharedFunctionsInitialized ); From e4535e09a00536ed872d416244fa32a6182c0b94 Mon Sep 17 00:00:00 2001 From: INSANEMODE Date: Fri, 2 Jun 2023 11:44:36 -0500 Subject: [PATCH 05/19] Patch game interface (#305) * remove extra set of parentheses in call to DisplaypopupsWaiter() * add missing event argument in call to GotoCoordImpl() * remove event arg from GotoCoordImpl() in t6 to match other game interface scripts --- GameFiles/GameInterface/_integration_t6.gsc | 2 +- GameFiles/GameInterface/_integration_t6zm_helper.gsc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GameFiles/GameInterface/_integration_t6.gsc b/GameFiles/GameInterface/_integration_t6.gsc index 18323bf83..8476f6060 100644 --- a/GameFiles/GameInterface/_integration_t6.gsc +++ b/GameFiles/GameInterface/_integration_t6.gsc @@ -302,7 +302,7 @@ GotoImpl( event, data ) } } -GotoCoordImpl( event, data ) +GotoCoordImpl( data ) { if ( !IsAlive( self ) ) { diff --git a/GameFiles/GameInterface/_integration_t6zm_helper.gsc b/GameFiles/GameInterface/_integration_t6zm_helper.gsc index 22ea47eec..59f4389d3 100644 --- a/GameFiles/GameInterface/_integration_t6zm_helper.gsc +++ b/GameFiles/GameInterface/_integration_t6zm_helper.gsc @@ -12,7 +12,7 @@ OnPlayerConnect() for ( ;; ) { level waittill( "connecting", player ); - player thread DisplaypopupsWaiter()(); + player thread DisplaypopupsWaiter(); } } From e843f839f5c442dcc14cf0a23bd9e863899a5acf Mon Sep 17 00:00:00 2001 From: RaidMax Date: Fri, 2 Jun 2023 16:35:00 -0500 Subject: [PATCH 06/19] adjust last seen format in game interface --- GameFiles/GameInterface/_integration_base.gsc | 6 +++--- GameFiles/GameInterface/_integration_shared.gsc | 4 ++-- GameFiles/GameInterface/_integration_t5zm.gsc | 2 +- GameFiles/GameInterface/_integration_t6zm_helper.gsc | 4 ++-- Plugins/ScriptPlugins/GameInterface.js | 2 +- SharedLibraryCore/PartialEntities/EFClient.cs | 3 +++ 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 6ff020844..c170bf183 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -347,7 +347,7 @@ QueueEvent( request, eventType, notifyEntity ) timedOut = "unset"; } - if ( timedOut == "set") + if ( timedOut == "set" ) { LogDebug( "Timed out waiting for response..." ); @@ -527,8 +527,8 @@ OnExecuteCommand( event ) OnSetClientDataCompleted( event ) { - // IW4MAdmin let us know it persisted (success or fail) - LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( event.data["status"] ) ); + data = ParseDataString( event.data ); + LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( data["status"] ) ); } CoerceUndefined( object ) diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index be81e2daa..21d7b01f1 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -134,7 +134,7 @@ PlayerSpawnEvents() self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel ); wait( 2.0 ); - self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection ); + self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection + " ago" ); } @@ -467,7 +467,7 @@ GetClosestPerformanceClientForTeam( sourceTeam, excluded ) else if ( candidateValue < closest ) { - scripts\_integration_base::LogDebug( candidateValue + " is the new best value "); + scripts\_integration_base::LogDebug( candidateValue + " is the new best value " ); choice = players[i]; closest = candidateValue; } diff --git a/GameFiles/GameInterface/_integration_t5zm.gsc b/GameFiles/GameInterface/_integration_t5zm.gsc index 0b2efd8cc..d56821f12 100644 --- a/GameFiles/GameInterface/_integration_t5zm.gsc +++ b/GameFiles/GameInterface/_integration_t5zm.gsc @@ -106,7 +106,7 @@ _GetPlayerFromClientNum( clientNum ) return undefined; } - players = GetPlayers("all"); + players = GetPlayers( "all" ); for ( i = 0; i < players.size; i++ ) { diff --git a/GameFiles/GameInterface/_integration_t6zm_helper.gsc b/GameFiles/GameInterface/_integration_t6zm_helper.gsc index 59f4389d3..befcf0a29 100644 --- a/GameFiles/GameInterface/_integration_t6zm_helper.gsc +++ b/GameFiles/GameInterface/_integration_t6zm_helper.gsc @@ -12,11 +12,11 @@ OnPlayerConnect() for ( ;; ) { level waittill( "connecting", player ); - player thread DisplaypopupsWaiter(); + player thread DisplayPopupsWaiter(); } } -DisplaypopupsWaiter() +DisplayPopupsWaiter() { self endon( "disconnect" ); self.ranknotifyqueue = []; diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 5fd82d993..2c56c0002 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -208,7 +208,7 @@ const plugin = { data = { level: client.level, clientId: client.clientId, - lastConnection: client.lastConnection, + lastConnection: client.timeSinceLastConnectionString, tag: tagMeta?.value ?? '', performance: clientStats?.performance ?? 200.0 }; diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index bfa35930d..c5c764f43 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -117,6 +117,9 @@ namespace SharedLibraryCore.Database.Models [NotMapped] public TeamType Team { get; set; } [NotMapped] public string TeamName { get; set; } + [NotMapped] + public string TimeSinceLastConnectionString => (DateTime.UtcNow - LastConnection).HumanizeForCurrentCulture(); + [NotMapped] // this is kinda dirty, but I need localizable level names public ClientPermission ClientPermission => new ClientPermission From 2fcbab9a3728351a1e66b4fe96eeb2c4833447be Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 3 Jun 2023 16:48:03 -0500 Subject: [PATCH 07/19] implement initial url request functionality for game interface --- Application/IW4MServer.cs | 1 - GameFiles/GameInterface/_integration_base.gsc | 127 +++++++++------- GameFiles/GameInterface/_integration_iw4x.gsc | 2 +- .../GameInterface/_integration_shared.gsc | 135 ++++++++++++++++++ IW4MAdmin.sln | 6 + Plugins/ScriptPlugins/GameInterface.js | 100 +++++++++++-- 6 files changed, 305 insertions(+), 66 deletions(-) diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 7c9b79dd3..bbde0c416 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -377,7 +377,6 @@ namespace IW4MAdmin if (E.Origin.State != ClientState.Connected) { E.Origin.State = ClientState.Connected; - E.Origin.LastConnection = DateTime.UtcNow; E.Origin.Connections += 1; ChatHistory.Add(new ChatInfo() diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index c170bf183..e918758ff 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -37,7 +37,7 @@ Setup() level.clientDataKey = "clientData"; level.eventTypes = spawnstruct(); - level.eventTypes.localClientEvent = "client_event"; + level.eventTypes.eventAvailable = "EventAvailable"; level.eventTypes.clientDataReceived = "ClientDataReceived"; level.eventTypes.clientDataRequested = "ClientDataRequested"; level.eventTypes.setClientDataRequested = "SetClientDataRequested"; @@ -70,6 +70,9 @@ Setup() _SetDvarIfUninitialized( level.eventBus.outVar, "" ); _SetDvarIfUninitialized( level.commonKeys.enabled, 1 ); _SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 ); + _SetDvarIfUninitialized( "GroupSeparatorChar", "" ); + _SetDvarIfUninitialized( "RecordSeparatorChar", "" ); + _SetDvarIfUninitialized( "UnitSeparatorChar", "" ); if ( GetDvarInt( level.commonKeys.enabled ) != 1 ) { @@ -77,35 +80,44 @@ Setup() } // start long running tasks - thread MonitorClientEvents(); + thread MonitorEvents(); thread MonitorBus(); } -////////////////////////////////// -// Client Methods -////////////////////////////////// - -MonitorClientEvents() +MonitorEvents() { level endon( level.eventTypes.gameEnd ); for ( ;; ) { - level waittill( level.eventTypes.localClientEvent, client ); + level waittill( level.eventTypes.eventAvailable, event ); - LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype ); + LogDebug( "Processing Event " + event.type + "-" + event.subtype ); - eventHandler = level.eventCallbacks[client.event.type]; + eventHandler = level.eventCallbacks[event.type]; if ( IsDefined( eventHandler ) ) { - client [[eventHandler]]( client.event ); - LogDebug( "notify client for " + client.event.type ); - client notify( level.eventTypes.localClientEvent, client.event ); - client notify( client.event.type, client.event ); + if ( IsDefined( event.entity ) ) + { + event.entity [[eventHandler]]( event ); + } + else + { + [[eventHandler]]( event ); + } + } + + if ( IsDefined( event.entity ) ) + { + LogDebug( "Notify client for " + event.type ); + event.entity notify( event.type, event ); + } + else + { + LogDebug( "Notify level for " + event.type ); + level notify( event.type, event ); } - - client.eventData = []; } } @@ -163,7 +175,7 @@ _Log( LogLevel, message ) { for( i = 0; i < level.logger._logger.size; i++ ) { - [[level.logger._logger[i]]]( LogLevel, message ); + [[level.logger._logger[i]]]( LogLevel, GetSubStr( message, 0, 1000 ) ); } } @@ -246,18 +258,20 @@ DecrementClientMeta( metaKey, decrementValue, clientId ) SetClientMeta( metaKey, metaValue, clientId, direction ) { - data = "key=" + metaKey + "|value=" + metaValue; + data = []; + data["key"] = metaKey; + data["value"] = metaValue; clientNumber = -1; if ( IsDefined ( clientId ) ) { - data = data + "|clientId=" + clientId; + data["clientId"] = clientId; clientNumber = -1; } if ( IsDefined( direction ) ) { - data = data + "|direction=" + direction; + data["direction"] = direction; } if ( IsPlayer( self ) ) @@ -292,9 +306,12 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) { request = "1"; } - - request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data; - return request; + + data = BuildDataString( data ); + groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); + request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data; + +eturn request; } MonitorBus() @@ -319,7 +336,8 @@ MonitorBus() } LogDebug( "-> " + eventString ); - NotifyClientEvent( strtok( eventString, ";" ) ); + groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); + NotifyEvent( strtok( eventString, groupSeparator ) ); SetDvar( level.eventBus.outVar, "" ); } @@ -361,7 +379,7 @@ QueueEvent( request, eventType, notifyEntity ) return; } - LogDebug("<- " + request ); + LogDebug( "<- " + request ); SetDvar( level.eventBus.inVar, request ); } @@ -374,13 +392,13 @@ ParseDataString( data ) return []; } - dataParts = strtok( data, "|" ); + dataParts = strtok( data, GetSubStr( GetDvar( "RecordSeparatorChar" ), 0, 1 ) ); dict = []; for ( i = 0; i < dataParts.size; i++ ) { part = dataParts[i]; - splitPart = strtok( part, "=" ); + splitPart = strtok( part, GetSubStr( GetDvar( "UnitSeparatorChar" ), 0, 1 ) ); key = splitPart[0]; value = splitPart[1]; dict[key] = value; @@ -390,6 +408,26 @@ ParseDataString( data ) return dict; } +BuildDataString( data ) +{ + if ( IsString( data ) ) + { + return data; + } + + dataString = ""; + keys = GetArrayKeys( data ); + unitSeparator = GetSubStr( GetDvar( "UnitSeparatorChar" ), 0, 1 ); + recordSeparator = GetSubStr( GetDvar( "RecordSeparatorChar" ), 0, 1 ); + + for ( i = 0; i < keys.size; i++ ) + { + dataString = dataString + keys[i] + unitSeparator + data[keys[i]] + recordSeparator; + } + + return dataString; +} + NotifyClientEventTimeout( eventType ) { // todo: make this actual eventing @@ -399,7 +437,7 @@ NotifyClientEventTimeout( eventType ) } } -NotifyClientEvent( eventInfo ) +NotifyEvent( eventInfo ) { origin = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[3] ) ); target = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[4] ) ); @@ -407,15 +445,10 @@ NotifyClientEvent( eventInfo ) event = spawnstruct(); event.type = eventInfo[1]; event.subtype = eventInfo[2]; - event.data = eventInfo[5]; + event.data = ParseDataString( eventInfo[5] ); event.origin = origin; event.target = target; - if ( IsDefined( event.data ) ) - { - LogDebug( "NotifyClientEvent->" + event.data ); - } - if ( int( eventInfo[3] ) != -1 && !IsDefined( origin ) ) { LogDebug( "origin is null but the slot id is " + int( eventInfo[3] ) ); @@ -425,23 +458,15 @@ NotifyClientEvent( eventInfo ) LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) ); } - if ( IsDefined( target ) ) + client = event.origin; + + if ( !IsDefined( client ) ) { client = event.target; } - else if ( IsDefined( origin ) ) - { - client = event.origin; - } - else - { - LogDebug( "Neither origin or target are set but we are a Client Event, aborting" ); - - return; - } - - client.event = event; - level notify( level.eventTypes.localClientEvent, client ); + + event.entity = client; + level notify( level.eventTypes.eventAvailable, event ); } AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite ) @@ -461,7 +486,6 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite ) OnClientDataReceived( event ) { - event.data = ParseDataString( event.data ); clientData = self.pers[level.clientDataKey]; if ( event.subtype == "Fail" ) @@ -497,7 +521,7 @@ OnClientDataReceived( event ) OnExecuteCommand( event ) { - data = ParseDataString( event.data ); + data = event.data; response = ""; command = level.clientCommandCallbacks[event.subtype]; @@ -527,8 +551,7 @@ OnExecuteCommand( event ) OnSetClientDataCompleted( event ) { - data = ParseDataString( event.data ); - LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( data["status"] ) ); + LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( event.data["status"] ) ); } CoerceUndefined( object ) diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index 71f9454ef..097b4ca72 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -85,7 +85,7 @@ WaitForClientEvents() for ( ;; ) { - self waittill( level.eventTypes.localClientEvent, event ); + self waittill( level.eventTypes.eventAvailable, event ); scripts\_integration_base::LogDebug( "Received client event " + event.type ); diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index 21d7b01f1..54f300c97 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -45,8 +45,14 @@ Setup() level.eventTypes.joinSpec = "joined_spectators"; level.eventTypes.spawned = "spawned_player"; level.eventTypes.gameEnd = "game_ended"; + + level.eventTypes.urlRequested = "UrlRequested"; + level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; + + level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; level.iw4madminIntegrationDefaultPerformance = 200; + level.notifyEntities = []; level notify( level.notifyTypes.sharedFunctionsInitialized ); level waittill( level.notifyTypes.gameFunctionsInitialized ); @@ -185,6 +191,135 @@ SaveTrackingMetrics() scripts\_integration_base::IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId ); } +// #region web requests + +RequestUrlObject( request ) +{ + return RequestUrl( request.url, request.method, request.body, request.headers, request ); +} + +RequestUrl( url, method, body, headers, webNotify ) +{ + if ( !IsDefined( webNotify ) ) + { + webNotify = SpawnStruct(); + webNotify.url = url; + webNotify.method = method; + webNotify.body = body; + webNotify.headers = headers; + } + + webNotify.index = GetNextNotifyEntity(); + + scripts\_integration_base::LogDebug( "next notify index is " + webNotify.index ); + level.notifyEntities[webNotify.index] = webNotify; + + data = []; + data["url"] = webNotify.url; + data["entity"] = webNotify.index; + + if ( IsDefined( method ) ) + { + data["method"] = method; + } + + if ( IsDefined( body ) ) + { + data["body"] = body; + } + + if ( IsDefined( headers ) ) + { + headerString = ""; + + keys = GetArrayKeys( headers ); + for ( i = 0; i < keys.size; i++ ) + { + headerString = headerString + keys[i] + ":" + headers[keys[i]] + ","; + } + + data["headers"] = headerString; + } + + webNotifyEvent = scripts\_integration_base::BuildEventRequest( true, level.eventTypes.urlRequested, "", webNotify.index, data ); + thread scripts\_integration_base::QueueEvent( webNotifyEvent, level.eventTypes.urlRequested, webNotify ); + webNotify thread WaitForUrlRequestComplete(); + + return webNotify; +} + +WaitForUrlRequestComplete() +{ + level endon( level.eventTypes.gameEnd ); + + timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.urlRequestCompleted ); + + if ( timeoutResult == level.eventBus.timeoutKey ) + { + scripts\_integration_base::LogWarning( "Request to " + self.url + " timed out" ); + self notify ( level.eventTypes.urlRequestCompleted, "error" ); + } + + scripts\_integration_base::LogDebug( "Request to " + self.url + " completed" ); + + //self delete(); + level.notifyEntities[self.index] = undefined; +} + +OnUrlRequestCompletedCallback( event ) +{ + if ( !IsDefined( event ) || !IsDefined( event.data ) ) + { + scripts\_integration_base::LogWarning( "Incomplete data for url request callback. [1]" ); + return; + } + + notifyEnt = event.data["entity"]; + response = event.data["response"]; + + if ( !IsDefined( notifyEnt ) || !IsDefined( response ) ) + { + scripts\_integration_base::LogWarning( "Incomplete data for url request callback. [2] " + scripts\_integration_base::CoerceUndefined( notifyEnt ) + " , " + scripts\_integration_base::CoerceUndefined( response ) ); + return; + } + + webNotify = level.notifyEntities[int( notifyEnt )]; + + if ( !IsDefined( webNotify.response ) ) + { + webNotify.response = response; + } + else + { + webNotify.response = webNotify.response + response; + } + + if ( int( event.data["remaining"] ) != 0 ) + { + scripts\_integration_base::LogDebug( "Additional data available for url request " + notifyEnt + " (" + event.data["remaining"] + " chunks remaining)" ); + return; + } + + scripts\_integration_base::LogDebug( "Notifying " + notifyEnt + " that url request completed" ); + webNotify notify( level.eventTypes.urlRequestCompleted, webNotify.response ); +} + +GetNextNotifyEntity() +{ + max = level.notifyEntities.size + 1; + + for ( i = 0; i < max; i++ ) + { + if ( !IsDefined( level.notifyEntities[i] ) ) + { + return i; + } + } +} + + +// #end region + // #region team balance OnPlayerDisconnect() diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 7d0cae409..b16c20b2c 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -72,6 +72,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mute", "Plugins\Mute\Mute.csproj", "{259824F3-D860-4233-91D6-FF73D4DD8B18}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameFiles", "GameFiles", "{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}" + ProjectSection(SolutionItems) = preProject + GameFiles\deploy.bat = GameFiles\deploy.bat + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterface", "{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55}" ProjectSection(SolutionItems) = preProject @@ -80,6 +83,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterf 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 + GameFiles\GameInterface\_integration_t5zm.gsc = GameFiles\GameInterface\_integration_t5zm.gsc + GameFiles\GameInterface\_integration_t6.gsc = GameFiles\GameInterface\_integration_t6.gsc + GameFiles\GameInterface\_integration_t6zm_helper.gsc = GameFiles\GameInterface\_integration_t6zm_helper.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 2c56c0002..e3c758286 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -3,33 +3,38 @@ const inDvar = 'sv_iw4madmin_in'; const outDvar = 'sv_iw4madmin_out'; const integrationEnabledDvar = 'sv_iw4madmin_integration_enabled'; const pollingRate = 300; +const groupSeparatorChar = '\x1d'; +const recordSeparatorChar = '\x1e'; +const unitSeparatorChar = '\x1f'; -const init = (registerNotify, serviceResolver, config) => { +const init = (registerNotify, serviceResolver, config, scriptHelper) => { registerNotify('IManagementEventSubscriptions.ClientStateInitialized', (clientEvent, _) => plugin.onClientEnteredMatch(clientEvent)); registerNotify('IGameServerEventSubscriptions.ServerValueReceived', (serverValueEvent, _) => plugin.onServerValueReceived(serverValueEvent)); registerNotify('IGameServerEventSubscriptions.ServerValueSetCompleted', (serverValueEvent, _) => plugin.onServerValueSetCompleted(serverValueEvent)); registerNotify('IGameServerEventSubscriptions.MonitoringStarted', (monitorStartEvent, _) => plugin.onServerMonitoringStart(monitorStartEvent)); registerNotify('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => plugin.onPenalty(penaltyEvent)); - plugin.onLoad(serviceResolver, config); + plugin.onLoad(serviceResolver, config, scriptHelper); return plugin; }; const plugin = { author: 'RaidMax', - version: '2.0', + version: '2.1', name: 'Game Interface', serviceResolver: null, eventManager: null, logger: null, commands: null, + scriptHelper: null, - onLoad: function (serviceResolver, config) { + onLoad: function (serviceResolver, config, scriptHelper) { this.serviceResolver = serviceResolver; this.eventManager = serviceResolver.resolveService('IManager'); this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']); this.commands = commands; this.config = config; + this.scriptHelper = scriptHelper; }, onClientEnteredMatch: function (clientEvent) { @@ -96,6 +101,10 @@ const plugin = { // loop restarts this.requestGetDvar(inDvar, serverValueEvent.server); }, + + onServerMonitoringStart: function (monitorStartEvent) { + this.initializeServer(monitorStartEvent.server); + }, initializeServer: function (server) { servers[server.id] = { @@ -287,17 +296,48 @@ const plugin = { } } + if (event.eventType === 'UrlRequested') { + const urlRequest = this.parseUrlRequest(event); + + this.logger.logDebug('Making gamescript web request {@Request}', urlRequest); + + this.scriptHelper.requestUrl(urlRequest, response => { + this.logger.logDebug('Got response for gamescript web request - {Response}', response); + + const max = 10; + this.logger.logDebug(`response length ${response.length}`); + let chunks = chunkString(response.replace(/"/gm, '\\"').replace(/[\n|\t]/gm, ''), 800); + if (chunks.length > max) + { + this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`); + chunks = chunks.slice(0, max); + } + this.logger.logDebug(`chunk size ${chunks.length}`); + + for (let i = 0; i < chunks.length; i++) { + this.sendEventMessage(server, false, 'UrlRequestCompleted', null, null, + null, { entity: event.data.entity, remaining: chunks.length - (i + 1), response: chunks[i]}); + } + }); + } + tokenSource.dispose(); return messageQueued; }, sendEventMessage: function (server, responseExpected, event, subtype, origin, target, data) { let targetClientNumber = -1; + let originClientNumber = -1; + if (target != null) { - targetClientNumber = target.ClientNumber; + targetClientNumber = target.clientNumber; } - const output = `${responseExpected ? '1' : '0'};${event};${subtype};${origin.ClientNumber};${targetClientNumber};${buildDataString(data)}`; + if (origin != null) { + originClientNumber = origin.clientNumber + } + + const output = `${responseExpected ? '1' : '0'}${groupSeparatorChar}${event}${groupSeparatorChar}${subtype}${groupSeparatorChar}${originClientNumber}${groupSeparatorChar}${targetClientNumber}${groupSeparatorChar}${buildDataString(data)}`; this.logger.logDebug('Queuing output for server {output}', output); servers[server.id].commandQueue.push(output); @@ -365,8 +405,35 @@ const plugin = { } }, - onServerMonitoringStart: function (monitorStartEvent) { - this.initializeServer(monitorStartEvent.server); + parseUrlRequest: function(event) { + const url = event.data?.url; + + if (url === undefined) { + this.logger.logWarning('No url provided for gamescript web request - {Event}', event); + return; + } + + const body = event.data?.body; + const method = event.data?.method || 'GET'; + const contentType = event.data?.contentType || 'text/plain'; + const headers = event.data?.headers; + + const dictionary = System.Collections.Generic.Dictionary(System.String, System.String); + const headerDict = new dictionary(); + + if (headers) { + const eachHeader = headers.split(','); + + for (let eachKeyValue of eachHeader) { + const keyValueSplit = eachKeyValue.split(':'); + if (keyValueSplit.length === 2) { + headerDict.add(keyValueSplit[0], keyValueSplit[1]); + } + } + } + + const script = importNamespace('IW4MAdmin.Application.Plugin.Script'); + return new script.ScriptPluginWebRequest(url, body, method, contentType, headerDict); } }; @@ -634,7 +701,7 @@ const parseEvent = (input) => { return {}; } - const eventInfo = input.split(';'); + const eventInfo = input.split(groupSeparatorChar); return { eventType: eventInfo[1], @@ -652,7 +719,7 @@ const buildDataString = data => { let formattedData = ''; for (let [key, value] of Object.entries(data)) { - formattedData += `${key}=${value}|`; + formattedData += `${key}${unitSeparatorChar}${value}${recordSeparatorChar}`; } return formattedData.slice(0, -1); @@ -664,11 +731,11 @@ const parseDataString = data => { } const dict = {}; - const split = data.split('|'); + const split = data.split(recordSeparatorChar); for (let i = 0; i < split.length; i++) { const segment = split[i]; - const keyValue = segment.split('='); + const keyValue = segment.split(unitSeparatorChar); if (keyValue.length !== 2) { continue; } @@ -689,3 +756,12 @@ const validateEnabled = (server, origin) => { const isEmpty = (value) => { return value == null || false || value === '' || value === 'null'; }; + +const chunkString = (str, chunkSize) => { + const result = []; + for (let i = 0; i < str.length; i += chunkSize) { + result.push(str.slice(i, i + chunkSize)); + } + + return result; +} From 3f0bdfe3a91abcbc367baab3575dec697d2507b7 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 3 Jun 2023 22:46:15 -0500 Subject: [PATCH 08/19] implement dynamic command registration through game interface --- .../Plugin/Script/ScriptPluginHelper.cs | 5 ++ Application/Plugin/Script/ScriptPluginV2.cs | 66 +++++++++------ GameFiles/GameInterface/_integration_base.gsc | 16 +++- .../GameInterface/_integration_shared.gsc | 80 ++++++++++++++++++- Plugins/ScriptPlugins/GameInterface.js | 28 +++++++ 5 files changed, 166 insertions(+), 29 deletions(-) diff --git a/Application/Plugin/Script/ScriptPluginHelper.cs b/Application/Plugin/Script/ScriptPluginHelper.cs index be15c580a..86b935f47 100644 --- a/Application/Plugin/Script/ScriptPluginHelper.cs +++ b/Application/Plugin/Script/ScriptPluginHelper.cs @@ -76,6 +76,11 @@ public class ScriptPluginHelper }); } + public void RegisterDynamicCommand(JsValue command) + { + _scriptPlugin.RegisterDynamicCommand(command.ToObject()); + } + private object RequestInternal(ScriptPluginWebRequest request) { var entered = false; diff --git a/Application/Plugin/Script/ScriptPluginV2.cs b/Application/Plugin/Script/ScriptPluginV2.cs index 19b70c790..26edcaa28 100644 --- a/Application/Plugin/Script/ScriptPluginV2.cs +++ b/Application/Plugin/Script/ScriptPluginV2.cs @@ -47,6 +47,7 @@ public class ScriptPluginV2 : IPluginV2 private readonly List _registeredCommandNames = new(); private readonly List _registeredInteractions = new(); private readonly Dictionary> _registeredEvents = new(); + private IManager _manager; private bool _firstInitialization = true; private record ScriptPluginDetails(string Name, string Author, string Version, @@ -112,8 +113,15 @@ public class ScriptPluginV2 : IPluginV2 }, _logger, _fileName, _onProcessingScript); } + public void RegisterDynamicCommand(object command) + { + var parsedCommand = ParseScriptCommandDetails(command); + RegisterCommand(_manager, parsedCommand.First()); + } + private async Task OnLoad(IManager manager, CancellationToken token) { + _manager = manager; var entered = false; try { @@ -253,8 +261,12 @@ public class ScriptPluginV2 : IPluginV2 command.Permission, command.TargetRequired, command.Arguments, Execute, command.SupportedGames); + manager.RemoveCommandByName(scriptCommand.Name); manager.AddAdditionalCommand(scriptCommand); - _registeredCommandNames.Add(scriptCommand.Name); + if (!_registeredCommandNames.Contains(scriptCommand.Name)) + { + _registeredCommandNames.Add(scriptCommand.Name); + } } private void ResetEngineState() @@ -480,6 +492,33 @@ public class ScriptPluginV2 : IPluginV2 } private static ScriptPluginDetails AsScriptPluginInstance(dynamic source) + { + var commandDetails = ParseScriptCommandDetails(source); + + var interactionDetails = Array.Empty(); + if (HasProperty(source, "interactions") && source.interactions is dynamic[]) + { + interactionDetails = ((dynamic[])source.interactions).Select(interaction => + { + var name = HasProperty(interaction, "name") && interaction.name is string + ? (string)interaction.name + : string.Empty; + var action = HasProperty(interaction, "action") && interaction.action is Delegate + ? (Delegate)interaction.action + : null; + + return new ScriptPluginInteractionDetails(name, action); + }).ToArray(); + } + + var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty; + var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty; + var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty; + + return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails); + } + + private static ScriptPluginCommandDetails[] ParseScriptCommandDetails(dynamic source) { var commandDetails = Array.Empty(); if (HasProperty(source, "commands") && source.commands is dynamic[]) @@ -513,7 +552,7 @@ public class ScriptPluginV2 : IPluginV2 (bool)command.targetRequired; var supportedGames = HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable - ? ((IEnumerable)command.supportedGames).Where(game => game?.ToString() is not null) + ? ((IEnumerable)command.supportedGames).Where(game => !string.IsNullOrEmpty(game?.ToString())) .Select(game => Enum.Parse(game.ToString()!)) : Array.Empty(); @@ -523,31 +562,10 @@ public class ScriptPluginV2 : IPluginV2 return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired, commandArgs, supportedGames, execute); - }).ToArray(); } - var interactionDetails = Array.Empty(); - if (HasProperty(source, "interactions") && source.interactions is dynamic[]) - { - interactionDetails = ((dynamic[])source.interactions).Select(interaction => - { - var name = HasProperty(interaction, "name") && interaction.name is string - ? (string)interaction.name - : string.Empty; - var action = HasProperty(interaction, "action") && interaction.action is Delegate - ? (Delegate)interaction.action - : null; - - return new ScriptPluginInteractionDetails(name, action); - }).ToArray(); - } - - var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty; - var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty; - var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty; - - return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails); + return commandDetails; } private static bool HasProperty(dynamic source, string name) diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index e918758ff..6df78ac30 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -295,6 +295,11 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) eventSubtype = "None"; } + if ( !IsDefined( entOrId ) ) + { + entOrId = "-1"; + } + if ( IsPlayer( entOrId ) ) { entOrId = entOrId getEntityNumber(); @@ -311,7 +316,7 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data; -eturn request; + return request; } MonitorBus() @@ -535,7 +540,14 @@ OnExecuteCommand( event ) if ( IsDefined( command ) ) { - response = executionContextEntity [[command]]( event, data ); + if ( IsDefined( executionContextEntity ) ) + { + response = executionContextEntity [[command]]( event, data ); + } + else + { + [[command]]( event ); + } } else { diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index 54f300c97..c88a1bb35 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -46,8 +46,9 @@ Setup() level.eventTypes.spawned = "spawned_player"; level.eventTypes.gameEnd = "game_ended"; - level.eventTypes.urlRequested = "UrlRequested"; - level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; + level.eventTypes.urlRequested = "UrlRequested"; + level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; + level.eventTypes.registerCommandRequested = "RegisterCommandRequested"; level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; @@ -191,6 +192,78 @@ SaveTrackingMetrics() scripts\_integration_base::IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId ); } +// #region register script command + +RegisterScriptCommandObject( command ) +{ + RegisterScriptCommand( command.eventKey, command.name, command.alias, command.description, command.minPermission, command.supportedGames, command.requiresTarget, command.handler ); +} + +RegisterScriptCommand( eventKey, name, alias, description, minPermission, supportedGames, requiresTarget, handler ) +{ + if ( !IsDefined( eventKey ) ) + { + scripts\_integration_base::LogError( "eventKey must be provided for script command" ); + return; + } + + data = []; + + data["eventKey"] = eventKey; + + if ( IsDefined( name ) ) + { + data["name"] = name; + } + else + { + scripts\_integration_base::LogError( "name must be provided for script command" ); + return; + } + + if ( IsDefined( alias ) ) + { + data["alias"] = alias; + } + + if ( IsDefined( description ) ) + { + data["description"] = description; + } + + if ( IsDefined( minPermission ) ) + { + data["minPermission"] = minPermission; + } + + if ( IsDefined( supportedGames ) ) + { + data["supportedGames"] = supportedGames; + } + + data["requiresTarget"] = false; + + if ( IsDefined( requiresTarget ) ) + { + data["requiresTarget"] = requiresTarget; + } + + if ( IsDefined( handler ) ) + { + level.clientCommandCallbacks[eventKey + "Execute"] = handler; + level.clientCommandRusAsTarget[eventKey + "Execute"] = data["requiresTarget"]; + } + else + { + scripts\_integration_base::LogWarning( "handler not defined for script command " + name ); + } + + commandRegisterRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.registerCommandRequested, "", undefined, data ); + thread scripts\_integration_base::QueueEvent( commandRegisterRequest, level.eventTypes.registerCommandRequested, undefined ); +} + +// #end region + // #region web requests RequestUrlObject( request ) @@ -262,7 +335,6 @@ WaitForUrlRequestComplete() scripts\_integration_base::LogDebug( "Request to " + self.url + " completed" ); - //self delete(); level.notifyEntities[self.index] = undefined; } @@ -315,6 +387,8 @@ GetNextNotifyEntity() return i; } } + + return max; } diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index e3c758286..8b073262f 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -320,6 +320,10 @@ const plugin = { } }); } + + if (event.eventType === 'RegisterCommandRequested') { + this.registerDynamicCommand(event); + } tokenSource.dispose(); return messageQueued; @@ -434,6 +438,30 @@ const plugin = { const script = importNamespace('IW4MAdmin.Application.Plugin.Script'); return new script.ScriptPluginWebRequest(url, body, method, contentType, headerDict); + }, + + registerDynamicCommand: function(event) { + const commandWrapper = { + commands: [{ + name: event.data['name'] || 'DEFAULT', + description: event.data['description'] || 'DEFAULT', + alias: event.data['alias'] || 'DEFAULT', + permission: event.data['minPermission'] || 'DEFAULT', + targetRequired: (event.data['targetRequired'] || '0') === '1', + supportedGames: (event.data['supportedGames'] || '').split(','), + + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.owner, gameEvent.origin)) { + return; + } + sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { + args: gameEvent.data + }); + } + }] + } + + this.scriptHelper.registerDynamicCommand(commandWrapper); } }; From eb8ea5e222a7b0933266dc83a8d41d89a7ea7867 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 4 Jun 2023 11:09:51 -0500 Subject: [PATCH 09/19] update pipeline to build develop --- DeploymentFiles/deployment-pipeline.yml | 448 ++++++++++++------------ 1 file changed, 228 insertions(+), 220 deletions(-) diff --git a/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml index 67bdc3b96..36533edeb 100644 --- a/DeploymentFiles/deployment-pipeline.yml +++ b/DeploymentFiles/deployment-pipeline.yml @@ -6,6 +6,7 @@ trigger: include: - release/pre - master + - develop pr: none @@ -20,227 +21,234 @@ variables: buildConfiguration: Stable isPreRelease: false -steps: -- task: UseDotNet@2 - displayName: 'Install .NET Core 6 SDK' - inputs: - packageType: 'sdk' - version: '6.0.x' - includePreviewVersions: true - -- task: NuGetToolInstaller@1 - -- task: PowerShell@2 - displayName: 'Setup Pre-Release configuration' - condition: eq(variables['Build.SourceBranch'], 'refs/heads/release/pre') - inputs: - targetType: 'inline' - script: | - echo '##vso[task.setvariable variable=releaseType]prerelease' - echo '##vso[task.setvariable variable=buildConfiguration]Prerelease' - echo '##vso[task.setvariable variable=isPreRelease]true' - failOnStderr: true - -- task: NuGetCommand@2 - displayName: 'Restore nuget packages' - inputs: - restoreSolution: '$(solution)' - -- task: PowerShell@2 - displayName: 'Preload external resources' - inputs: - targetType: 'inline' - script: | - Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)' - md -Force lib\open-iconic\font\css - wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss - cd lib\open-iconic\font\css - (Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot' - -- task: VSBuild@1 - displayName: 'Build projects' - inputs: - solution: '$(solution)' - msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber)' - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' - -- task: PowerShell@2 - displayName: 'Bundle JS Files' - inputs: - targetType: 'inline' - script: | - Write-Host 'Getting dotnet bundle' - wget http://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip -o $(Build.Repository.LocalPath)\dotnet-bundle.zip - Write-Host 'Unzipping download' - Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath) - Write-Host 'Executing dotnet-bundle' - $(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json - $(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore' - -- task: DotNetCoreCLI@2 - displayName: 'Publish projects' - inputs: - command: 'publish' - publishWebProjects: false - projects: | - **/WebfrontCore.csproj - **/Application.csproj - arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Build.BuildNumber)' - zipAfterPublish: false - modifyOutputPath: false +jobs: + - job: Build + steps: + - task: UseDotNet@2 + displayName: 'Install .NET Core 6 SDK' + inputs: + packageType: 'sdk' + version: '6.0.x' + includePreviewVersions: true -- task: PowerShell@2 - displayName: 'Run publish script 1' - inputs: - filePath: 'DeploymentFiles/PostPublish.ps1' - arguments: '$(outputFolder)' - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)' - -- task: BatchScript@1 - displayName: 'Run publish script 2' - inputs: - filename: 'Application\BuildScripts\PostPublish.bat' - workingFolder: '$(Build.Repository.LocalPath)' - arguments: '$(outputFolder) $(Build.Repository.LocalPath)' - failOnStandardError: true - -- task: PowerShell@2 - displayName: 'Download dos2unix for line endings' - inputs: - targetType: 'inline' - script: 'wget https://raidmax.org/downloads/dos2unix.exe' - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - -- task: CmdLine@2 - displayName: 'Convert Linux start script line endings' - inputs: - script: | - echo changing to encoding for linux start script - dos2unix $(outputFolder)\StartIW4MAdmin.sh - dos2unix $(outputFolder)\UpdateIW4MAdmin.sh - echo creating website version filename - @echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt - workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - -- task: CopyFiles@2 - displayName: 'Move script plugins into publish directory' - inputs: - SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins' - Contents: '*.js' - TargetFolder: '$(outputFolder)\Plugins' - -- task: CopyFiles@2 - displayName: 'Move binary plugins into publish directory' - inputs: - SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\' - Contents: '*.dll' - TargetFolder: '$(outputFolder)\Plugins' - -- task: CmdLine@2 - displayName: 'Move webfront resources into publish directory' - inputs: - script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot' - workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins' - failOnStderr: true - -- task: CmdLine@2 - displayName: 'Move gamescript files into publish directory' - inputs: - script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' - workingDirectory: '$(Build.Repository.LocalPath)' - failOnStderr: true - -- task: ArchiveFiles@2 - displayName: 'Generate final zip file' - inputs: - rootFolderOrFile: '$(outputFolder)' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' - replaceExistingArchive: true - -- task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' - artifact: 'IW4MAdmin-$(Build.BuildNumber).zip' - -- task: FtpUpload@2 - displayName: 'Upload zip file to website' - inputs: - credentialsOption: 'inputs' - serverUrl: '$(FTPUrl)' - username: '$(FTPUsername)' - password: '$(FTPPassword)' - rootDirectory: '$(Build.ArtifactStagingDirectory)' - filePatterns: '*.zip' - remoteDirectory: 'IW4MAdmin/Download' - clean: false - cleanContents: false - preservePaths: false - trustSSL: false - -- task: FtpUpload@2 - displayName: 'Upload version info to website' - inputs: - credentialsOption: 'inputs' - serverUrl: '$(FTPUrl)' - username: '$(FTPUsername)' - password: '$(FTPPassword)' - rootDirectory: '$(Build.ArtifactStagingDirectory)' - filePatterns: 'version_$(releaseType).txt' - remoteDirectory: 'IW4MAdmin' - clean: false - cleanContents: false - preservePaths: false - trustSSL: false - -- task: GitHubRelease@1 - displayName: 'Make GitHub release' - inputs: - gitHubConnection: 'github.com_RaidMax' - repositoryName: 'RaidMax/IW4M-Admin' - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: 'userSpecifiedTag' - tag: '$(Build.BuildNumber)-$(releaseType)' - title: 'IW4MAdmin $(Build.BuildNumber) ($(releaseType))' - assets: '$(Build.ArtifactStagingDirectory)/*.zip' - isPreRelease: $(isPreRelease) - releaseNotesSource: 'inline' - releaseNotesInline: 'todo' - changeLogCompareToRelease: 'lastNonDraftRelease' - changeLogType: 'commitBased' - -- task: PowerShell@2 - displayName: 'Update master version' - inputs: - targetType: 'inline' - script: | - $payload = @{ - 'current-version-$(releaseType)' = '$(Build.BuildNumber)' - 'jwt-secret' = '$(JWTSecret)' - } | ConvertTo-Json - + - task: NuGetToolInstaller@1 + + - task: PowerShell@2 + displayName: 'Setup Pre-Release configuration' + condition: eq(variables['Build.SourceBranch'], 'refs/heads/release/pre') + inputs: + targetType: 'inline' + script: | + echo '##vso[task.setvariable variable=releaseType]prerelease' + echo '##vso[task.setvariable variable=buildConfiguration]Prerelease' + echo '##vso[task.setvariable variable=isPreRelease]true' + failOnStderr: true - $params = @{ - Uri = 'http://api.raidmax.org:5000/version' - Method = 'POST' - Body = $payload - ContentType = 'application/json' - } + - task: NuGetCommand@2 + displayName: 'Restore nuget packages' + inputs: + restoreSolution: '$(solution)' + + - task: PowerShell@2 + displayName: 'Preload external resources' + inputs: + targetType: 'inline' + script: | + Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)' + md -Force lib\open-iconic\font\css + wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss + cd lib\open-iconic\font\css + (Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss + failOnStderr: true + workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot' + + - task: VSBuild@1 + displayName: 'Build projects' + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber)' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + + - task: PowerShell@2 + displayName: 'Bundle JS Files' + inputs: + targetType: 'inline' + script: | + Write-Host 'Getting dotnet bundle' + wget http://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip -o $(Build.Repository.LocalPath)\dotnet-bundle.zip + Write-Host 'Unzipping download' + Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath) + Write-Host 'Executing dotnet-bundle' + $(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json + $(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json + failOnStderr: true + workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore' + + - task: DotNetCoreCLI@2 + displayName: 'Publish projects' + inputs: + command: 'publish' + publishWebProjects: false + projects: | + **/WebfrontCore.csproj + **/Application.csproj + arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Build.BuildNumber)' + zipAfterPublish: false + modifyOutputPath: false + + - task: PowerShell@2 + displayName: 'Run publish script 1' + inputs: + filePath: 'DeploymentFiles/PostPublish.ps1' + arguments: '$(outputFolder)' + failOnStderr: true + workingDirectory: '$(Build.Repository.LocalPath)' + + - task: BatchScript@1 + displayName: 'Run publish script 2' + inputs: + filename: 'Application\BuildScripts\PostPublish.bat' + workingFolder: '$(Build.Repository.LocalPath)' + arguments: '$(outputFolder) $(Build.Repository.LocalPath)' + failOnStandardError: true + + - task: PowerShell@2 + displayName: 'Download dos2unix for line endings' + inputs: + targetType: 'inline' + script: 'wget https://raidmax.org/downloads/dos2unix.exe' + failOnStderr: true + workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' + - job: Transform + steps: + - task: CmdLine@2 + displayName: 'Convert Linux start script line endings' + inputs: + script: | + echo changing to encoding for linux start script + dos2unix $(outputFolder)\StartIW4MAdmin.sh + dos2unix $(outputFolder)\UpdateIW4MAdmin.sh + echo creating website version filename + @echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt + workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - Invoke-RestMethod @params + - task: CopyFiles@2 + displayName: 'Move script plugins into publish directory' + inputs: + SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins' + Contents: '*.js' + TargetFolder: '$(outputFolder)\Plugins' + + - task: CopyFiles@2 + displayName: 'Move binary plugins into publish directory' + inputs: + SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\' + Contents: '*.dll' + TargetFolder: '$(outputFolder)\Plugins' + + - task: CmdLine@2 + displayName: 'Move webfront resources into publish directory' + inputs: + script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot' + workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins' + failOnStderr: true + + - task: CmdLine@2 + displayName: 'Move gamescript files into publish directory' + inputs: + script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' + workingDirectory: '$(Build.Repository.LocalPath)' + failOnStderr: true + - job: Artifact + steps: + - task: ArchiveFiles@2 + displayName: 'Generate final zip file' + inputs: + rootFolderOrFile: '$(outputFolder)' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' + replaceExistingArchive: true + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' + artifact: 'IW4MAdmin-$(Build.BuildNumber).zip' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish artifact for analysis' + inputs: + targetPath: '$(outputFolder)' + artifact: 'IW4MAdmin.$(buildConfiguration)' + publishLocation: 'pipeline' -- task: PublishPipelineArtifact@1 - displayName: 'Publish artifact for analysis' - inputs: - targetPath: '$(outputFolder)' - artifact: 'IW4MAdmin.$(buildConfiguration)' - publishLocation: 'pipeline' + - job: Publish + condition: and(succeeded(), ne(variables['Build.SourceBranch'], 'refs/heads/develop')) + steps: + - task: FtpUpload@2 + displayName: 'Upload zip file to website' + inputs: + credentialsOption: 'inputs' + serverUrl: '$(FTPUrl)' + username: '$(FTPUsername)' + password: '$(FTPPassword)' + rootDirectory: '$(Build.ArtifactStagingDirectory)' + filePatterns: '*.zip' + remoteDirectory: 'IW4MAdmin/Download' + clean: false + cleanContents: false + preservePaths: false + trustSSL: false + + - task: FtpUpload@2 + displayName: 'Upload version info to website' + inputs: + credentialsOption: 'inputs' + serverUrl: '$(FTPUrl)' + username: '$(FTPUsername)' + password: '$(FTPPassword)' + rootDirectory: '$(Build.ArtifactStagingDirectory)' + filePatterns: 'version_$(releaseType).txt' + remoteDirectory: 'IW4MAdmin' + clean: false + cleanContents: false + preservePaths: false + trustSSL: false + + - task: GitHubRelease@1 + displayName: 'Make GitHub release' + inputs: + gitHubConnection: 'github.com_RaidMax' + repositoryName: 'RaidMax/IW4M-Admin' + action: 'create' + target: '$(Build.SourceVersion)' + tagSource: 'userSpecifiedTag' + tag: '$(Build.BuildNumber)-$(releaseType)' + title: 'IW4MAdmin $(Build.BuildNumber) ($(releaseType))' + assets: '$(Build.ArtifactStagingDirectory)/*.zip' + isPreRelease: $(isPreRelease) + releaseNotesSource: 'inline' + releaseNotesInline: 'todo' + changeLogCompareToRelease: 'lastNonDraftRelease' + changeLogType: 'commitBased' + + - task: PowerShell@2 + displayName: 'Update master version' + inputs: + targetType: 'inline' + script: | + $payload = @{ + 'current-version-$(releaseType)' = '$(Build.BuildNumber)' + 'jwt-secret' = '$(JWTSecret)' + } | ConvertTo-Json + + + $params = @{ + Uri = 'http://api.raidmax.org:5000/version' + Method = 'POST' + Body = $payload + ContentType = 'application/json' + } + + Invoke-RestMethod @params From 5a22a759a8dc1a9e9845dc136869fe98f0811c69 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 4 Jun 2023 11:16:33 -0500 Subject: [PATCH 10/19] add job dependency to pipeline --- DeploymentFiles/deployment-pipeline.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml index 36533edeb..04e1412c1 100644 --- a/DeploymentFiles/deployment-pipeline.yml +++ b/DeploymentFiles/deployment-pipeline.yml @@ -35,7 +35,7 @@ jobs: - task: PowerShell@2 displayName: 'Setup Pre-Release configuration' - condition: eq(variables['Build.SourceBranch'], 'refs/heads/release/pre') + condition: or(eq(variables['Build.SourceBranch'], 'refs/heads/release/pre'), eq(variables['Build.SourceBranch'], 'refs/heads/develop')) inputs: targetType: 'inline' script: | @@ -120,7 +120,9 @@ jobs: script: 'wget https://raidmax.org/downloads/dos2unix.exe' failOnStderr: true workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' + - job: Transform + dependsOn: Build steps: - task: CmdLine@2 displayName: 'Convert Linux start script line endings' @@ -160,7 +162,9 @@ jobs: script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' workingDirectory: '$(Build.Repository.LocalPath)' failOnStderr: true + - job: Artifact + dependsOn: Transform steps: - task: ArchiveFiles@2 displayName: 'Generate final zip file' @@ -184,6 +188,7 @@ jobs: publishLocation: 'pipeline' - job: Publish + dependsOn: Artifact condition: and(succeeded(), ne(variables['Build.SourceBranch'], 'refs/heads/develop')) steps: - task: FtpUpload@2 From 50593f5a93b7ee7edf5a763bc272852ee9c07cbb Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 4 Jun 2023 11:29:19 -0500 Subject: [PATCH 11/19] revert pipeline back to one job --- DeploymentFiles/deployment-pipeline.yml | 138 ++++++++++++------------ 1 file changed, 66 insertions(+), 72 deletions(-) diff --git a/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml index 04e1412c1..f08670f32 100644 --- a/DeploymentFiles/deployment-pipeline.yml +++ b/DeploymentFiles/deployment-pipeline.yml @@ -22,7 +22,7 @@ variables: isPreRelease: false jobs: - - job: Build + - job: Build & Deploy steps: - task: UseDotNet@2 displayName: 'Install .NET Core 6 SDK' @@ -112,7 +112,7 @@ jobs: workingFolder: '$(Build.Repository.LocalPath)' arguments: '$(outputFolder) $(Build.Repository.LocalPath)' failOnStandardError: true - + - task: PowerShell@2 displayName: 'Download dos2unix for line endings' inputs: @@ -120,78 +120,69 @@ jobs: script: 'wget https://raidmax.org/downloads/dos2unix.exe' failOnStderr: true workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - - - job: Transform - dependsOn: Build - steps: - - task: CmdLine@2 - displayName: 'Convert Linux start script line endings' - inputs: - script: | - echo changing to encoding for linux start script - dos2unix $(outputFolder)\StartIW4MAdmin.sh - dos2unix $(outputFolder)\UpdateIW4MAdmin.sh - echo creating website version filename - @echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt - workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - - - task: CopyFiles@2 - displayName: 'Move script plugins into publish directory' - inputs: - SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins' - Contents: '*.js' - TargetFolder: '$(outputFolder)\Plugins' - - - task: CopyFiles@2 - displayName: 'Move binary plugins into publish directory' - inputs: - SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\' - Contents: '*.dll' - TargetFolder: '$(outputFolder)\Plugins' - - - task: CmdLine@2 - displayName: 'Move webfront resources into publish directory' - inputs: - script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot' - workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins' - failOnStderr: true - - - task: CmdLine@2 - displayName: 'Move gamescript files into publish directory' - inputs: - script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' - workingDirectory: '$(Build.Repository.LocalPath)' - failOnStderr: true - - - job: Artifact - dependsOn: Transform - steps: - - task: ArchiveFiles@2 - displayName: 'Generate final zip file' - inputs: - rootFolderOrFile: '$(outputFolder)' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' - replaceExistingArchive: true - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' - artifact: 'IW4MAdmin-$(Build.BuildNumber).zip' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish artifact for analysis' - inputs: - targetPath: '$(outputFolder)' - artifact: 'IW4MAdmin.$(buildConfiguration)' - publishLocation: 'pipeline' + + - task: CmdLine@2 + displayName: 'Convert Linux start script line endings' + inputs: + script: | + echo changing to encoding for linux start script + dos2unix $(outputFolder)\StartIW4MAdmin.sh + dos2unix $(outputFolder)\UpdateIW4MAdmin.sh + echo creating website version filename + @echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt + workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' + + - task: CopyFiles@2 + displayName: 'Move script plugins into publish directory' + inputs: + SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins' + Contents: '*.js' + TargetFolder: '$(outputFolder)\Plugins' + + - task: CopyFiles@2 + displayName: 'Move binary plugins into publish directory' + inputs: + SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\' + Contents: '*.dll' + TargetFolder: '$(outputFolder)\Plugins' + + - task: CmdLine@2 + displayName: 'Move webfront resources into publish directory' + inputs: + script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot' + workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins' + failOnStderr: true + + - task: CmdLine@2 + displayName: 'Move gamescript files into publish directory' + inputs: + script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' + workingDirectory: '$(Build.Repository.LocalPath)' + failOnStderr: true + + - task: ArchiveFiles@2 + displayName: 'Generate final zip file' + inputs: + rootFolderOrFile: '$(outputFolder)' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' + replaceExistingArchive: true + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' + artifact: 'IW4MAdmin-$(Build.BuildNumber).zip' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish artifact for analysis' + inputs: + targetPath: '$(outputFolder)' + artifact: 'IW4MAdmin.$(buildConfiguration)' + publishLocation: 'pipeline' - - job: Publish - dependsOn: Artifact - condition: and(succeeded(), ne(variables['Build.SourceBranch'], 'refs/heads/develop')) - steps: - task: FtpUpload@2 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') displayName: 'Upload zip file to website' inputs: credentialsOption: 'inputs' @@ -207,6 +198,7 @@ jobs: trustSSL: false - task: FtpUpload@2 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') displayName: 'Upload version info to website' inputs: credentialsOption: 'inputs' @@ -222,6 +214,7 @@ jobs: trustSSL: false - task: GitHubRelease@1 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') displayName: 'Make GitHub release' inputs: gitHubConnection: 'github.com_RaidMax' @@ -239,6 +232,7 @@ jobs: changeLogType: 'commitBased' - task: PowerShell@2 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') displayName: 'Update master version' inputs: targetType: 'inline' From e7f5e6a8411eb71da4c90ee267e7ae623a28f5f0 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 4 Jun 2023 11:30:26 -0500 Subject: [PATCH 12/19] fix job name --- DeploymentFiles/deployment-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml index f08670f32..0a2d73533 100644 --- a/DeploymentFiles/deployment-pipeline.yml +++ b/DeploymentFiles/deployment-pipeline.yml @@ -22,7 +22,7 @@ variables: isPreRelease: false jobs: - - job: Build & Deploy + - job: Build_Deploy steps: - task: UseDotNet@2 displayName: 'Install .NET Core 6 SDK' From 2340e30c2de9b07ed05bd419b806a568831636c0 Mon Sep 17 00:00:00 2001 From: INSANEMODE Date: Sun, 4 Jun 2023 19:07:52 -0500 Subject: [PATCH 13/19] gameinterface additions (#306) * -add waittill_any_timeout to _integration_t6.gsc - thread event handler calls in monitorEvents * - add WaitTillAnyTimeout to iw5 - remove unneeded thread from event handlers - change WaitTillAnyTimeout in t6 to use a wrapper --- GameFiles/GameInterface/_integration_iw5.gsc | 7 ++++++- GameFiles/GameInterface/_integration_t6.gsc | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/GameFiles/GameInterface/_integration_iw5.gsc b/GameFiles/GameInterface/_integration_iw5.gsc index 16140fd84..4619656cb 100644 --- a/GameFiles/GameInterface/_integration_iw5.gsc +++ b/GameFiles/GameInterface/_integration_iw5.gsc @@ -20,7 +20,7 @@ Setup() level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; - + level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; RegisterClientCommands(); level notify( level.notifyTypes.gameFunctionsInitialized ); @@ -71,6 +71,11 @@ GetXuidWrapper() return self GetXUID(); } +WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 ) +{ + return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ); +} + ////////////////////////////////// // Command Implementations ///////////////////////////////// diff --git a/GameFiles/GameInterface/_integration_t6.gsc b/GameFiles/GameInterface/_integration_t6.gsc index 8476f6060..e049b9cc4 100644 --- a/GameFiles/GameInterface/_integration_t6.gsc +++ b/GameFiles/GameInterface/_integration_t6.gsc @@ -27,6 +27,7 @@ Setup() level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; + level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; RegisterClientCommands(); @@ -98,6 +99,11 @@ GetXuidWrapper() return self GetXUID(); } +WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 ) +{ + return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ); +} + ////////////////////////////////// // Command Implementations ///////////////////////////////// From ad89ecb39d608efb046703f90ee5123b63c60609 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 6 Jun 2023 12:08:58 -0500 Subject: [PATCH 14/19] add example module to game interface. convert gi command registration to a iw4madmin request --- GameFiles/GameInterface/_integration_base.gsc | 4 +- GameFiles/GameInterface/_integration_iw4x.gsc | 4 +- .../GameInterface/_integration_shared.gsc | 33 ++++++- GameFiles/GameInterface/example_module.gsc | 85 +++++++++++++++++++ IW4MAdmin.sln | 1 + Integrations/Cod/CodRConConnection.cs | 24 ++++-- Plugins/ScriptPlugins/GameInterface.js | 38 +++++++-- 7 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 GameFiles/GameInterface/example_module.gsc diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 6df78ac30..62f20396c 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -542,11 +542,11 @@ OnExecuteCommand( event ) { if ( IsDefined( executionContextEntity ) ) { - response = executionContextEntity [[command]]( event, data ); + response = executionContextEntity thread [[command]]( event, data ); } else { - [[command]]( event ); + thread [[command]]( event ); } } else diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index 097b4ca72..19a9b1dfc 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -52,9 +52,9 @@ OnPlayerConnect() if ( player IsTestClient() ) { // we don't want to track bots - continue; + continue; } - + player thread SetPersistentData(); player thread WaitForClientEvents(); } diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index c88a1bb35..bb217a67f 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -49,11 +49,14 @@ Setup() level.eventTypes.urlRequested = "UrlRequested"; level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; level.eventTypes.registerCommandRequested = "RegisterCommandRequested"; + level.eventTypes.getCommandsRequested = "GetCommandsRequested"; level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; + level.eventCallbacks[level.eventTypes.getCommandsRequested] = ::OnCommandsRequestedCallback; level.iw4madminIntegrationDefaultPerformance = 200; level.notifyEntities = []; + level.customCommands = []; level notify( level.notifyTypes.sharedFunctionsInitialized ); level waittill( level.notifyTypes.gameFunctionsInitialized ); @@ -194,6 +197,32 @@ SaveTrackingMetrics() // #region register script command +OnCommandsRequestedCallback( event ) +{ + scripts\_integration_base::LogDebug( "Get commands requested" ); + thread SendCommands( event.data["name"] ); +} + +SendCommands( commandName ) +{ + level endon( level.eventTypes.gameEnd ); + + for ( i = 0; i < level.customCommands.size; i++ ) + { + data = level.customCommands[i]; + + if ( IsDefined( commandName ) && commandName != data["name"] ) + { + continue; + } + + scripts\_integration_base::LogDebug( "Sending custom command " + ( i + 1 ) + "/" + level.customCommands.size + ": " + data["name"] ); + commandRegisterRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.registerCommandRequested, "", undefined, data ); + // not threading here as there might be a lot of commands to register + scripts\_integration_base::QueueEvent( commandRegisterRequest, level.eventTypes.registerCommandRequested, undefined ); + } +} + RegisterScriptCommandObject( command ) { RegisterScriptCommand( command.eventKey, command.name, command.alias, command.description, command.minPermission, command.supportedGames, command.requiresTarget, command.handler ); @@ -258,8 +287,7 @@ RegisterScriptCommand( eventKey, name, alias, description, minPermission, suppor scripts\_integration_base::LogWarning( "handler not defined for script command " + name ); } - commandRegisterRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.registerCommandRequested, "", undefined, data ); - thread scripts\_integration_base::QueueEvent( commandRegisterRequest, level.eventTypes.registerCommandRequested, undefined ); + level.customCommands[level.customCommands.size] = data; } // #end region @@ -391,7 +419,6 @@ GetNextNotifyEntity() return max; } - // #end region // #region team balance diff --git a/GameFiles/GameInterface/example_module.gsc b/GameFiles/GameInterface/example_module.gsc new file mode 100644 index 000000000..ede3fd243 --- /dev/null +++ b/GameFiles/GameInterface/example_module.gsc @@ -0,0 +1,85 @@ +Init() +{ + // this gives the game interface time to setup + waittillframeend; + thread ModuleSetup(); +} + +ModuleSetup() +{ + // waiting until the game specific functions are ready + level waittill( level.notifyTypes.gameFunctionsInitialized ); + + RegisterCustomCommands(); +} + +RegisterCustomCommands() +{ + command = SpawnStruct(); + + // unique key for each command (how iw4madmin identifies the command) + command.eventKey = "PrintLineCommand"; + + // name of the command (cannot conflict with existing command names) + command.name = "println"; + + // short version of the command (cannot conflcit with existing command aliases) + command.alias = "pl"; + + // description of what the command does + command.description = "prints line to game"; + + // minimum permision required to execute + // valid values: User, Trusted, Moderator, Administrator, SeniorAdmin, Owner + command.minPermission = "Trusted"; + + // games the command is supported on + // separate with comma or don't define for all + // valid values: IW3, IW4, IW5, IW6, T4, T5, T6, T7, SHG1, CSGO, H1 + command.supportedGames = "IW4,IW5,T5,T6"; + + // indicates if a target player must be provided to execvute on + command.requiresTarget = false; + + // code to run when the command is executed + command.handler = ::PrintLnCommandCallback; + + // register the command with integration to be send to iw4madmin + scripts\_integration_shared::RegisterScriptCommandObject( command ); + + // you can also register via parameters + scripts\_integration_shared::RegisterScriptCommand( "AffirmationCommand", "affirm", "af", "provide affirmations", "User", undefined, false, ::AffirmationCommandCallback ); +} + +PrintLnCommandCallback( event ) +{ + if ( IsDefined( event.data["args"] ) ) + { + IPrintLnBold( event.data["args"] ); + return; + } + + scripts\_integration_base::LogDebug( "No data was provided for PrintLnCallback" ); +} + +AffirmationCommandCallback( event, _ ) +{ + level endon( level.eventTypes.gameEnd ); + + request = SpawnStruct(); + request.url = "https://www.affirmations.dev"; + request.method = "GET"; + + // If making a post request you can also provide more data + // request.body = "Body of the post message"; + // request.headers = []; + // request.headers["Authorization"] = "api-key"; + + scripts\_integration_shared::RequestUrlObject( request ); + request waittill( level.eventTypes.urlRequestCompleted, response ); + + // horrible json parsing.. but it's just an example + parsedResponse = strtok( response, "\"" ); + + self IPrintLnBold ( "^5" + parsedResponse[parsedResponse.size - 2] ); +} diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index b16c20b2c..1b05d620f 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -86,6 +86,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterf GameFiles\GameInterface\_integration_t5zm.gsc = GameFiles\GameInterface\_integration_t5zm.gsc GameFiles\GameInterface\_integration_t6.gsc = GameFiles\GameInterface\_integration_t6.gsc GameFiles\GameInterface\_integration_t6zm_helper.gsc = GameFiles\GameInterface\_integration_t6zm_helper.gsc + GameFiles\GameInterface\example_module.gsc = GameFiles\GameInterface\example_module.gsc EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}" diff --git a/Integrations/Cod/CodRConConnection.cs b/Integrations/Cod/CodRConConnection.cs index b670e6ba1..780600940 100644 --- a/Integrations/Cod/CodRConConnection.cs +++ b/Integrations/Cod/CodRConConnection.cs @@ -147,6 +147,18 @@ namespace Integrations.Cod { var convertedRConPassword = ConvertEncoding(RConPassword); var convertedParameters = ConvertEncoding(parameters); + byte SafeConversion(char c) + { + try + { + return Convert.ToByte(c); + } + + catch + { + return (byte)'.'; + } + }; switch (type) { @@ -154,30 +166,30 @@ namespace Integrations.Cod waitForResponse = true; payload = string .Format(_config.CommandPrefixes.RConGetDvar, convertedRConPassword, - convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + convertedParameters + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.SET_DVAR: payload = string .Format(_config.CommandPrefixes.RConSetDvar, convertedRConPassword, - convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + convertedParameters + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.COMMAND: payload = string .Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, - convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + convertedParameters + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.GET_STATUS: waitForResponse = true; - payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray(); + payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.GET_INFO: waitForResponse = true; - payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray(); + payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(SafeConversion).ToArray(); break; case StaticHelpers.QueryType.COMMAND_STATUS: waitForResponse = true; payload = string.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0") - .Select(Convert.ToByte).ToArray(); + .Select(SafeConversion).ToArray(); break; } } diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 8b073262f..01c6ce9ca 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -92,9 +92,7 @@ const plugin = { const input = serverState.inQueue.shift(); // if we queued an event then the next loop will be at the value set complete - if (await this.processEventMessage(input, serverValueEvent.server)) { - // return; - } + await this.processEventMessage(input, serverValueEvent.server); } this.logger.logDebug('loop complete'); @@ -134,7 +132,8 @@ const plugin = { serverState.enabled = true; serverState.running = true; serverState.initializationInProgress = false; - + + this.sendEventMessage(responseEvent.server, true, 'GetCommandsRequested', null, null, null, {}); this.requestGetDvar(inDvar, responseEvent.server); }, @@ -145,7 +144,9 @@ const plugin = { const serverState = servers[responseEvent.server.id]; serverState.outQueue.shift(); - if (responseEvent.server.connectedClients.count === 0) { + const utilities = importNamespace('SharedLibraryCore.Utilities'); + + if (responseEvent.server.connectedClients.count === 0 && !utilities.isDevelopment) { // no clients connected so we don't need to query serverState.running = false; return; @@ -304,9 +305,22 @@ const plugin = { this.scriptHelper.requestUrl(urlRequest, response => { this.logger.logDebug('Got response for gamescript web request - {Response}', response); + if ( typeof response !== 'string' && !(response instanceof String) ) + { + response = JSON.stringify(response); + } + const max = 10; this.logger.logDebug(`response length ${response.length}`); - let chunks = chunkString(response.replace(/"/gm, '\\"').replace(/[\n|\t]/gm, ''), 800); + + let quoteReplace = '\\"'; + // todo: may be more than just T6 + if (server.gameCode === 'T6') + { + quoteReplace = '\\\\"'; + } + + let chunks = chunkString(response.replace(/"/gm, quoteReplace).replace(/[\n|\t]/gm, ''), 800); if (chunks.length > max) { this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`); @@ -454,9 +468,15 @@ const plugin = { if (!validateEnabled(gameEvent.owner, gameEvent.origin)) { return; } - sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { - args: gameEvent.data - }); + + if (gameEvent.data === '--reload') + { + this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, { name: gameEvent.extra.name }); + } else { + sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { + args: gameEvent.data + }); + } } }] } From 871f8d75df28ce4a7ed2e094eb7b38ea102520d5 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 6 Jun 2023 17:56:12 -0500 Subject: [PATCH 15/19] implement bus mode for game interface to allow files for bus data transfer --- .../Plugin/Script/ScriptPluginHelper.cs | 2 +- GameFiles/GameInterface/_integration_base.gsc | 60 +++++++++++-- GameFiles/GameInterface/_integration_iw4x.gsc | 27 +++++- .../GameInterface/_integration_shared.gsc | 28 +++++- GameFiles/GameInterface/example_module.gsc | 5 +- Plugins/ScriptPlugins/GameInterface.js | 90 +++++++++++++++++-- 6 files changed, 190 insertions(+), 22 deletions(-) diff --git a/Application/Plugin/Script/ScriptPluginHelper.cs b/Application/Plugin/Script/ScriptPluginHelper.cs index 86b935f47..5d8937e74 100644 --- a/Application/Plugin/Script/ScriptPluginHelper.cs +++ b/Application/Plugin/Script/ScriptPluginHelper.cs @@ -67,7 +67,7 @@ public class ScriptPluginHelper try { await Task.Delay(delayMs, _manager.CancellationToken); - _scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined)); + _scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined })); } catch { diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 62f20396c..2cf4196b9 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -21,13 +21,29 @@ Setup() level.commonFunctions.setDvar = "SetDvarIfUninitialized"; level.commonFunctions.getPlayerFromClientNum = "GetPlayerFromClientNum"; level.commonFunctions.waittillNotifyOrTimeout = "WaittillNotifyOrTimeout"; + level.commonFunctions.getInboundData = "GetInboundData"; + level.commonFunctions.getOutboundData = "GetOutboundData"; + level.commonFunctions.setInboundData = "SetInboundData"; + level.commonFunctions.setOutboundData = "SetOutboundData"; level.overrideMethods = []; level.overrideMethods[level.commonFunctions.setDvar] = scripts\_integration_base::NotImplementedFunction; level.overrideMethods[level.commonFunctions.getPlayerFromClientNum] = ::_GetPlayerFromClientNum; + level.overrideMethods[level.commonFunctions.getInboundData] = ::_GetInboundData; + level.overrideMethods[level.commonFunctions.getOutboundData] = ::_GetOutboundData; + level.overrideMethods[level.commonFunctions.setInboundData] = ::_SetInboundData; + level.overrideMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData; + + level.busMethods = []; + level.busMethods[level.commonFunctions.getInboundData] = ::_GetInboundData; + level.busMethods[level.commonFunctions.getOutboundData] = ::_GetOutboundData; + level.busMethods[level.commonFunctions.setInboundData] = ::_SetInboundData; + level.busMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData; level.commonKeys = spawnstruct(); level.commonKeys.enabled = "sv_iw4madmin_integration_enabled"; + level.commonKeys.busMode = "sv_iw4madmin_integration_busmode"; + level.commonKeys.busDir = "sv_iw4madmin_integration_busdir"; level.notifyTypes = spawnstruct(); level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized"; @@ -69,6 +85,8 @@ Setup() _SetDvarIfUninitialized( level.eventBus.inVar, "" ); _SetDvarIfUninitialized( level.eventBus.outVar, "" ); _SetDvarIfUninitialized( level.commonKeys.enabled, 1 ); + _SetDvarIfUninitialized( level.commonKeys.busMode, "rcon" ); + _SetDvarIfUninitialized( level.commonKeys.busdir, "" ); _SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 ); _SetDvarIfUninitialized( "GroupSeparatorChar", "" ); _SetDvarIfUninitialized( "RecordSeparatorChar", "" ); @@ -157,6 +175,26 @@ _GetPlayerFromClientNum( clientNum ) return undefined; } +_GetInboundData() +{ + return GetDvar( level.eventBus.inVar ); +} + +_GetOutboundData() +{ + return GetDvar( level.eventBus.outVar ); +} + +_SetInboundData( data ) +{ + return SetDvar( level.eventBus.inVar, data ); +} + +_SetOutboundData( data ) +{ + return SetDvar( level.eventBus.outVar, data ); +} + // Not every game can output to console or even game log. // Adds a very basic logging system that every // game specific script can extend.accumulate @@ -322,29 +360,35 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) MonitorBus() { level endon( level.eventTypes.gameEnd ); + + [[level.overrideMethods[level.commonFunctions.SetInboundData]]]( "" ); + [[level.overrideMethods[level.commonFunctions.SetOutboundData]]]( "" ); for( ;; ) { wait ( 0.1 ); // check to see if IW4MAdmin is ready to receive more data - if ( getDvar( level.eventBus.inVar ) == "" ) + inVal = [[level.busMethods[level.commonFunctions.getInboundData]]](); + + if ( !IsDefined( inVal ) || inVal == "" ) { level notify( "bus_ready" ); } - eventString = getDvar( level.eventBus.outVar ); + eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]](); - if ( eventString == "" ) + if ( !IsDefined( eventString ) || eventString == "" ) { continue; } + LogDebug( "-> " + eventString ); groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); NotifyEvent( strtok( eventString, groupSeparator ) ); - SetDvar( level.eventBus.outVar, "" ); + [[level.busMethods[level.commonFunctions.SetOutboundData]]]( "" ); } } @@ -356,11 +400,11 @@ QueueEvent( request, eventType, notifyEntity ) maxWait = level.eventBus.timeout * 1000; // 30 seconds timedOut = ""; - while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait ) + while ( [[level.busMethods[level.commonFunctions.getInboundData]]]() != "" && ( GetTime() - start ) < maxWait ) { level [[level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout]]]( "bus_ready", 1 ); - if ( GetDvar( level.eventBus.inVar ) != "" ) + if ( [[level.busMethods[level.commonFunctions.getInboundData]]]() != "" ) { LogDebug( "A request is already in progress..." ); timedOut = "set"; @@ -379,14 +423,14 @@ QueueEvent( request, eventType, notifyEntity ) notifyEntity NotifyClientEventTimeout( eventType ); } - SetDvar( level.eventBus.inVar, "" ); + [[level.busMethods[level.commonFunctions.SetInboundData]]]( "" ); return; } LogDebug( "<- " + request ); - SetDvar( level.eventBus.inVar, request ); + [[level.busMethods[level.commonFunctions.setInboundData]]]( request ); } ParseDataString( data ) diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index 19a9b1dfc..26e94565f 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -28,7 +28,12 @@ Setup() level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = ::BackupRestoreClientKillStreakData; level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; - + + level.overrideMethods[level.commonFunctions.getInboundData] = ::GetInboundData; + level.overrideMethods[level.commonFunctions.getOutboundData] = ::GetOutboundData; + level.overrideMethods[level.commonFunctions.setInboundData] = ::SetInboundData; + level.overrideMethods[level.commonFunctions.setOutboundData] = ::SetOutboundData; + RegisterClientCommands(); level notify( level.notifyTypes.gameFunctionsInitialized ); @@ -97,6 +102,26 @@ WaitForClientEvents() } } +GetInboundData() +{ + return FileRead( level.eventBus.inVar); +} + +GetOutboundData() +{ + return FileRead( level.eventBus.outVar ); +} + +SetInboundData( data ) +{ + FileWrite( level.eventBus.inVar, data, "write" ); +} + +SetOutboundData( data ) +{ + FileWrite(level.eventBus.outVar, data, "write" ); +} + GetMaxClients() { return level.maxClients; diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index bb217a67f..c4336d421 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -50,9 +50,11 @@ Setup() level.eventTypes.urlRequestCompleted = "UrlRequestCompleted"; level.eventTypes.registerCommandRequested = "RegisterCommandRequested"; level.eventTypes.getCommandsRequested = "GetCommandsRequested"; + level.eventTypes.getBusModeRequested = "GetBusModeRequested"; - level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; - level.eventCallbacks[level.eventTypes.getCommandsRequested] = ::OnCommandsRequestedCallback; + level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback; + level.eventCallbacks[level.eventTypes.getCommandsRequested] = ::OnCommandsRequestedCallback; + level.eventCallbacks[level.eventTypes.getBusModeRequested] = ::OnBusModeRequestedCallback; level.iw4madminIntegrationDefaultPerformance = 200; level.notifyEntities = []; @@ -195,6 +197,28 @@ SaveTrackingMetrics() scripts\_integration_base::IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId ); } +OnBusModeRequestedCallback( event ) +{ + data = []; + data["mode"] = GetDvar( level.commonKeys.busMode ); + data["directory"] = GetDvar( level.commonKeys.busDir ); + + scripts\_integration_base::LogDebug( "Bus mode requested" ); + + busModeRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.getBusModeRequested, "", undefined, data ); + scripts\_integration_base::QueueEvent( busModeRequest, level.eventTypes.getBusModeRequested, undefined ); + + scripts\_integration_base::LogDebug( "Bus mode updated" ); + + if ( GetDvar( level.commonKeys.busMode ) == "file" || GetDvar( level.commonKeys.busDir ) != "" ) + { + level.busMethods[level.commonFunctions.getInboundData] = level.overrideMethods[level.commonFunctions.getInboundData]; + level.busMethods[level.commonFunctions.getOutboundData] = level.overrideMethods[level.commonFunctions.getOutboundData]; + level.busMethods[level.commonFunctions.setInboundData] = level.overrideMethods[level.commonFunctions.setInboundData]; + level.busMethods[level.commonFunctions.setOutboundData] = level.overrideMethods[level.commonFunctions.setOutboundData]; + } +} + // #region register script command OnCommandsRequestedCallback( event ) diff --git a/GameFiles/GameInterface/example_module.gsc b/GameFiles/GameInterface/example_module.gsc index ede3fd243..4dc8cceb8 100644 --- a/GameFiles/GameInterface/example_module.gsc +++ b/GameFiles/GameInterface/example_module.gsc @@ -81,5 +81,8 @@ AffirmationCommandCallback( event, _ ) // horrible json parsing.. but it's just an example parsedResponse = strtok( response, "\"" ); - self IPrintLnBold ( "^5" + parsedResponse[parsedResponse.size - 2] ); + if ( IsPlayer( self ) ) + { + self IPrintLnBold ( "^5" + parsedResponse[parsedResponse.size - 2] ); + } } diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 01c6ce9ca..28d56b1a1 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -7,6 +7,9 @@ const groupSeparatorChar = '\x1d'; const recordSeparatorChar = '\x1e'; const unitSeparatorChar = '\x1f'; +let busMode = 'rcon'; +let busDir = ''; + const init = (registerNotify, serviceResolver, config, scriptHelper) => { registerNotify('IManagementEventSubscriptions.ClientStateInitialized', (clientEvent, _) => plugin.onClientEnteredMatch(clientEvent)); registerNotify('IGameServerEventSubscriptions.ServerValueReceived', (serverValueEvent, _) => plugin.onServerValueReceived(serverValueEvent)); @@ -70,6 +73,9 @@ const plugin = { }, onServerValueSetCompleted: async function (serverValueEvent) { + this.logger.logDebug('Set {dvarName}={dvarValue} success={success} from {server}', serverValueEvent.valueName, + serverValueEvent.value, serverValueEvent.success, serverValueEvent.server.id); + if (serverValueEvent.valueName !== inDvar && serverValueEvent.valueName !== outDvar) { this.logger.logDebug('Ignoring set complete of {name}', serverValueEvent.valueName); return; @@ -132,7 +138,8 @@ const plugin = { serverState.enabled = true; serverState.running = true; serverState.initializationInProgress = false; - + + this.sendEventMessage(responseEvent.server, true, 'GetBusModeRequested', null, null, null, {}); this.sendEventMessage(responseEvent.server, true, 'GetCommandsRequested', null, null, null, {}); this.requestGetDvar(inDvar, responseEvent.server); }, @@ -189,8 +196,8 @@ const plugin = { let messageQueued = false; const event = parseEvent(input); - this.logger.logDebug('Processing input... {eventType} {subType} {data} {clientNumber}', event.eventType, - event.subType, event.data.toString(), event.clientNumber); + this.logger.logDebug('Processing input... {eventType} {subType} {@data} {clientNumber}', event.eventType, + event.subType, event.data, event.clientNumber); const metaService = this.serviceResolver.ResolveService('IMetaServiceV2'); const threading = importNamespace('System.Threading'); @@ -305,8 +312,7 @@ const plugin = { this.scriptHelper.requestUrl(urlRequest, response => { this.logger.logDebug('Got response for gamescript web request - {Response}', response); - if ( typeof response !== 'string' && !(response instanceof String) ) - { + if (typeof response !== 'string' && !(response instanceof String)) { response = JSON.stringify(response); } @@ -315,14 +321,12 @@ const plugin = { let quoteReplace = '\\"'; // todo: may be more than just T6 - if (server.gameCode === 'T6') - { + if (server.gameCode === 'T6') { quoteReplace = '\\\\"'; } let chunks = chunkString(response.replace(/"/gm, quoteReplace).replace(/[\n|\t]/gm, ''), 800); - if (chunks.length > max) - { + if (chunks.length > max) { this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`); chunks = chunks.slice(0, max); } @@ -338,6 +342,14 @@ const plugin = { if (event.eventType === 'RegisterCommandRequested') { this.registerDynamicCommand(event); } + + if (event.eventType === 'GetBusModeRequested') { + if (event.data?.directory && event.data?.mode) { + busMode = event.data.mode; + busDir = event.data.directory; + this.logger.logDebug('Setting bus mode to {mode} {dir}', busMode, busDir); + } + } tokenSource.dispose(); return messageQueued; @@ -363,6 +375,37 @@ const plugin = { requestGetDvar: function (dvarName, server) { const serverState = servers[server.id]; + + if (dvarName !== integrationEnabledDvar && busMode === 'file') { + this.scriptHelper.requestNotifyAfterDelay(250, () => { + const io = importNamespace('System.IO'); + serverState.outQueue.push({}); + try { + const content = io.File.ReadAllText(`${busDir}/${dvarName}`); + plugin.onServerValueReceived({ + server: server, + source: server, + success: true, + response: { + name: dvarName, + value: content + } + }); + } catch (e) { + plugin.logger.logError('Could not get bus data {exception}', e.toString()); + plugin.onServerValueReceived({ + server: server, + success: false, + response: { + name: dvarName + } + }); + } + }); + + return; + } + const serverEvents = importNamespace('SharedLibraryCore.Events.Server'); const requestEvent = new serverEvents.ServerValueRequestEvent(dvarName, server); requestEvent.delayMs = pollingRate; @@ -393,6 +436,35 @@ const plugin = { requestSetDvar: function (dvarName, dvarValue, server) { const serverState = servers[server.id]; + + if ( busMode === 'file' ) { + this.scriptHelper.requestNotifyAfterDelay(250, async () => { + const io = importNamespace('System.IO'); + try { + const path = `${busDir}/${dvarName}`; + plugin.logger.logDebug('writing {value} to {file}', dvarValue, path); + io.File.WriteAllText(path, dvarValue); + serverState.outQueue.push({}); + await plugin.onServerValueSetCompleted({ + server: server, + source: server, + success: true, + value: dvarValue, + valueName: dvarName, + }); + } catch (e) { + plugin.logger.logError('Could not set bus data {exception}', e.toString()); + await plugin.onServerValueSetCompleted({ + server: server, + success: false, + valueName: dvarName, + value: dvarValue + }); + } + }) + + return; + } const serverEvents = importNamespace('SharedLibraryCore.Events.Server'); const requestEvent = new serverEvents.ServerValueSetRequestEvent(dvarName, dvarValue, server); From f79ba6466cd4836eca5031b61753b8a952907225 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Wed, 7 Jun 2023 16:15:54 -0500 Subject: [PATCH 16/19] tweak game interface bus mode --- GameFiles/GameInterface/_integration_base.gsc | 37 ++++++++------ GameFiles/GameInterface/_integration_iw4x.gsc | 16 +++--- .../GameInterface/_integration_shared.gsc | 2 + Plugins/ScriptPlugins/GameInterface.js | 49 +++++++++++++++---- 4 files changed, 70 insertions(+), 34 deletions(-) diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index 2cf4196b9..dffb68c5b 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -41,9 +41,11 @@ Setup() level.busMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData; level.commonKeys = spawnstruct(); - level.commonKeys.enabled = "sv_iw4madmin_integration_enabled"; - level.commonKeys.busMode = "sv_iw4madmin_integration_busmode"; - level.commonKeys.busDir = "sv_iw4madmin_integration_busdir"; + level.commonKeys.enabled = "sv_iw4madmin_integration_enabled"; + level.commonKeys.busMode = "sv_iw4madmin_integration_busmode"; + level.commonKeys.busDir = "sv_iw4madmin_integration_busdir"; + level.eventBus.inLocation = ""; + level.eventBus.outLocation = ""; level.notifyTypes = spawnstruct(); level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized"; @@ -175,22 +177,22 @@ _GetPlayerFromClientNum( clientNum ) return undefined; } -_GetInboundData() +_GetInboundData( location ) { return GetDvar( level.eventBus.inVar ); } -_GetOutboundData() +_GetOutboundData( location ) { return GetDvar( level.eventBus.outVar ); } -_SetInboundData( data ) +_SetInboundData( location, data ) { return SetDvar( level.eventBus.inVar, data ); } -_SetOutboundData( data ) +_SetOutboundData( location, data ) { return SetDvar( level.eventBus.outVar, data ); } @@ -361,22 +363,25 @@ MonitorBus() { level endon( level.eventTypes.gameEnd ); - [[level.overrideMethods[level.commonFunctions.SetInboundData]]]( "" ); - [[level.overrideMethods[level.commonFunctions.SetOutboundData]]]( "" ); + level.eventBus.inLocation = level.eventBus.inVar + "_" + GetDvar( "net_port" ); + level.eventBus.outLocation = level.eventBus.outVar + "_" + GetDvar( "net_port" ); + + [[level.overrideMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" ); + [[level.overrideMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" ); for( ;; ) { wait ( 0.1 ); // check to see if IW4MAdmin is ready to receive more data - inVal = [[level.busMethods[level.commonFunctions.getInboundData]]](); + inVal = [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ); if ( !IsDefined( inVal ) || inVal == "" ) { level notify( "bus_ready" ); } - eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]](); + eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]]( level.eventBus.outLocation ); if ( !IsDefined( eventString ) || eventString == "" ) { @@ -388,7 +393,7 @@ MonitorBus() groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); NotifyEvent( strtok( eventString, groupSeparator ) ); - [[level.busMethods[level.commonFunctions.SetOutboundData]]]( "" ); + [[level.busMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" ); } } @@ -400,11 +405,11 @@ QueueEvent( request, eventType, notifyEntity ) maxWait = level.eventBus.timeout * 1000; // 30 seconds timedOut = ""; - while ( [[level.busMethods[level.commonFunctions.getInboundData]]]() != "" && ( GetTime() - start ) < maxWait ) + while ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" && ( GetTime() - start ) < maxWait ) { level [[level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout]]]( "bus_ready", 1 ); - if ( [[level.busMethods[level.commonFunctions.getInboundData]]]() != "" ) + if ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" ) { LogDebug( "A request is already in progress..." ); timedOut = "set"; @@ -423,14 +428,14 @@ QueueEvent( request, eventType, notifyEntity ) notifyEntity NotifyClientEventTimeout( eventType ); } - [[level.busMethods[level.commonFunctions.SetInboundData]]]( "" ); + [[level.busMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" ); return; } LogDebug( "<- " + request ); - [[level.busMethods[level.commonFunctions.setInboundData]]]( request ); + [[level.busMethods[level.commonFunctions.setInboundData]]]( level.eventBus.inLocation, request ); } ParseDataString( data ) diff --git a/GameFiles/GameInterface/_integration_iw4x.gsc b/GameFiles/GameInterface/_integration_iw4x.gsc index 26e94565f..9a717d0eb 100644 --- a/GameFiles/GameInterface/_integration_iw4x.gsc +++ b/GameFiles/GameInterface/_integration_iw4x.gsc @@ -102,24 +102,24 @@ WaitForClientEvents() } } -GetInboundData() +GetInboundData( location ) { - return FileRead( level.eventBus.inVar); + return FileRead( location ); } -GetOutboundData() +GetOutboundData( location ) { - return FileRead( level.eventBus.outVar ); + return FileRead( location ); } -SetInboundData( data ) +SetInboundData( location, data ) { - FileWrite( level.eventBus.inVar, data, "write" ); + FileWrite( location, data, "write" ); } -SetOutboundData( data ) +SetOutboundData( location, data ) { - FileWrite(level.eventBus.outVar, data, "write" ); + FileWrite( location, data, "write" ); } GetMaxClients() diff --git a/GameFiles/GameInterface/_integration_shared.gsc b/GameFiles/GameInterface/_integration_shared.gsc index c4336d421..fc2b30073 100644 --- a/GameFiles/GameInterface/_integration_shared.gsc +++ b/GameFiles/GameInterface/_integration_shared.gsc @@ -202,6 +202,8 @@ OnBusModeRequestedCallback( event ) data = []; data["mode"] = GetDvar( level.commonKeys.busMode ); data["directory"] = GetDvar( level.commonKeys.busDir ); + data["inLocation"] = level.eventBus.inLocation; + data["outLocation"] = level.eventBus.outLocation; scripts\_integration_base::LogDebug( "Bus mode requested" ); diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 28d56b1a1..135ee1fa2 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -1,8 +1,7 @@ const servers = {}; -const inDvar = 'sv_iw4madmin_in'; -const outDvar = 'sv_iw4madmin_out'; +let inDvar = 'sv_iw4madmin_in'; +let outDvar = 'sv_iw4madmin_out'; const integrationEnabledDvar = 'sv_iw4madmin_integration_enabled'; -const pollingRate = 300; const groupSeparatorChar = '\x1d'; const recordSeparatorChar = '\x1e'; const unitSeparatorChar = '\x1f'; @@ -15,6 +14,7 @@ const init = (registerNotify, serviceResolver, config, scriptHelper) => { registerNotify('IGameServerEventSubscriptions.ServerValueReceived', (serverValueEvent, _) => plugin.onServerValueReceived(serverValueEvent)); registerNotify('IGameServerEventSubscriptions.ServerValueSetCompleted', (serverValueEvent, _) => plugin.onServerValueSetCompleted(serverValueEvent)); registerNotify('IGameServerEventSubscriptions.MonitoringStarted', (monitorStartEvent, _) => plugin.onServerMonitoringStart(monitorStartEvent)); + registerNotify('IGameEventSubscriptions.MatchStarted', (matchStartEvent, _) => plugin.onMatchStart(matchStartEvent)); registerNotify('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => plugin.onPenalty(penaltyEvent)); plugin.onLoad(serviceResolver, config, scriptHelper); @@ -30,14 +30,31 @@ const plugin = { logger: null, commands: null, scriptHelper: null, + configWrapper: null, + config: { + pollingRate: 300 + }, - onLoad: function (serviceResolver, config, scriptHelper) { + onLoad: function (serviceResolver, configWrapper, scriptHelper) { this.serviceResolver = serviceResolver; this.eventManager = serviceResolver.resolveService('IManager'); this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']); this.commands = commands; - this.config = config; + this.configWrapper = configWrapper; this.scriptHelper = scriptHelper; + + const storedConfig = this.configWrapper.getValue('config', newConfig => { + if (newConfig) { + plugin.logger.logInformation('{Name} config reloaded.', plugin.name); + plugin.config = newConfig; + } + }); + + if (storedConfig != null) { + this.config = storedConfig + } else { + this.configWrapper.setValue('config', this.config); + } }, onClientEnteredMatch: function (clientEvent) { @@ -110,6 +127,11 @@ const plugin = { this.initializeServer(monitorStartEvent.server); }, + onMatchStart: function (matchStartEvent) { + busMode = 'rcon'; + this.sendEventMessage(matchStartEvent.server, true, 'GetBusModeRequested', null, null, null, {}); + }, + initializeServer: function (server) { servers[server.id] = { enabled: false, @@ -138,6 +160,9 @@ const plugin = { serverState.enabled = true; serverState.running = true; serverState.initializationInProgress = false; + + // todo: this might not work for all games + responseEvent.server.rconParser.configuration.floodProtectInterval = 150; this.sendEventMessage(responseEvent.server, true, 'GetBusModeRequested', null, null, null, {}); this.sendEventMessage(responseEvent.server, true, 'GetCommandsRequested', null, null, null, {}); @@ -346,7 +371,11 @@ const plugin = { if (event.eventType === 'GetBusModeRequested') { if (event.data?.directory && event.data?.mode) { busMode = event.data.mode; - busDir = event.data.directory; + busDir = event.data.directory.replace('\'', '').replace('"', ''); + if (event.data?.inLocation && event.data?.outLocation) { + inDvar = event.data?.inLocation; + outDvar = event.data?.outLocation; + } this.logger.logDebug('Setting bus mode to {mode} {dir}', busMode, busDir); } } @@ -408,7 +437,7 @@ const plugin = { const serverEvents = importNamespace('SharedLibraryCore.Events.Server'); const requestEvent = new serverEvents.ServerValueRequestEvent(dvarName, server); - requestEvent.delayMs = pollingRate; + requestEvent.delayMs = this.config.pollingRate; requestEvent.timeoutMs = 2000; requestEvent.source = this.name; @@ -418,7 +447,7 @@ const plugin = { const diff = new Date().getTime() - end.getTime(); if (diff < extraDelay) { - requestEvent.delayMs = (extraDelay - diff) + pollingRate; + requestEvent.delayMs = (extraDelay - diff) + this.config.pollingRate; this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs); } } @@ -468,7 +497,7 @@ const plugin = { const serverEvents = importNamespace('SharedLibraryCore.Events.Server'); const requestEvent = new serverEvents.ServerValueSetRequestEvent(dvarName, dvarValue, server); - requestEvent.delayMs = pollingRate; + requestEvent.delayMs = this.config.pollingRate; requestEvent.timeoutMs = 2000; requestEvent.source = this.name; @@ -478,7 +507,7 @@ const plugin = { const diff = new Date().getTime() - end.getTime(); if (diff < extraDelay) { - requestEvent.delayMs = (extraDelay - diff) + pollingRate; + requestEvent.delayMs = (extraDelay - diff) + this.config.pollingRate; this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs); } } From 789981346afdea7925052f4de02d7c12410acd19 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 8 Jun 2023 15:16:42 -0500 Subject: [PATCH 17/19] update Jint package --- Application/Application.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/Application.csproj b/Application/Application.csproj index fffa5b9ce..4c478d2b9 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -24,7 +24,7 @@ - + all From bcb063730c23d8889a8264cef254961799cbdbbb Mon Sep 17 00:00:00 2001 From: RaidMax Date: Thu, 8 Jun 2023 16:26:26 -0500 Subject: [PATCH 18/19] fix game interface bus issue and limit dynamic script command reload to owner --- Plugins/ScriptPlugins/GameInterface.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 135ee1fa2..19cf64dbd 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -1,11 +1,13 @@ const servers = {}; -let inDvar = 'sv_iw4madmin_in'; -let outDvar = 'sv_iw4madmin_out'; +const inDvar = 'sv_iw4madmin_in'; +const outDvar = 'sv_iw4madmin_out'; const integrationEnabledDvar = 'sv_iw4madmin_integration_enabled'; const groupSeparatorChar = '\x1d'; const recordSeparatorChar = '\x1e'; const unitSeparatorChar = '\x1f'; +let busFileIn = ''; +let busFileOut = ''; let busMode = 'rcon'; let busDir = ''; @@ -373,8 +375,8 @@ const plugin = { busMode = event.data.mode; busDir = event.data.directory.replace('\'', '').replace('"', ''); if (event.data?.inLocation && event.data?.outLocation) { - inDvar = event.data?.inLocation; - outDvar = event.data?.outLocation; + busFileIn = event.data?.inLocation; + busFileOut = event.data?.outLocation; } this.logger.logDebug('Setting bus mode to {mode} {dir}', busMode, busDir); } @@ -410,7 +412,7 @@ const plugin = { const io = importNamespace('System.IO'); serverState.outQueue.push({}); try { - const content = io.File.ReadAllText(`${busDir}/${dvarName}`); + const content = io.File.ReadAllText(`${busDir}/${fileForDvar(dvarName)}`); plugin.onServerValueReceived({ server: server, source: server, @@ -470,7 +472,7 @@ const plugin = { this.scriptHelper.requestNotifyAfterDelay(250, async () => { const io = importNamespace('System.IO'); try { - const path = `${busDir}/${dvarName}`; + const path = `${busDir}/${fileForDvar(dvarName)}`; plugin.logger.logDebug('writing {value} to {file}', dvarValue, path); io.File.WriteAllText(path, dvarValue); serverState.outQueue.push({}); @@ -570,8 +572,7 @@ const plugin = { return; } - if (gameEvent.data === '--reload') - { + if (gameEvent.data === '--reload' && gameEvent.origin.level === 'Owner') { this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, { name: gameEvent.extra.name }); } else { sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { @@ -914,3 +915,11 @@ const chunkString = (str, chunkSize) => { return result; } + +const fileForDvar = (dvar) => { + if (dvar === inDvar) { + return busFileIn; + } + + return busFileOut; +} From 3f11a4fe9fe3e2422d16c268b78590218f9029ca Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 10 Jun 2023 09:57:58 -0500 Subject: [PATCH 19/19] update release notes template --- DeploymentFiles/deployment-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml index 0a2d73533..e8463b150 100644 --- a/DeploymentFiles/deployment-pipeline.yml +++ b/DeploymentFiles/deployment-pipeline.yml @@ -227,7 +227,7 @@ jobs: assets: '$(Build.ArtifactStagingDirectory)/*.zip' isPreRelease: $(isPreRelease) releaseNotesSource: 'inline' - releaseNotesInline: 'todo' + releaseNotesInline: 'Automated rolling release - changelog below. [Updating Instructions](https://github.com/RaidMax/IW4M-Admin/wiki/Getting-Started#updating)' changeLogCompareToRelease: 'lastNonDraftRelease' changeLogType: 'commitBased'