From 871f8d75df28ce4a7ed2e094eb7b38ea102520d5 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 6 Jun 2023 17:56:12 -0500 Subject: [PATCH] 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);