From 1700b7da91a55ebf35d9acd28ad1ba36aa7a1f85 Mon Sep 17 00:00:00 2001 From: xerxes-at <5236639+xerxes-at@users.noreply.github.com> Date: Fri, 20 May 2022 00:04:34 +0200 Subject: [PATCH] PlutoIW5 support for the Game Interface and improvements to the GSC part of it. (#242) * Improvements to the GSC part of the Game Interface * Adds compatibility with PlutoIW5 with minimal changes. * Fixes issues when commands are called from the web interface when the used profile is not on the server. * New Debug output when the target or origin of a command is sent by IW4MAdmin but not found in-game. * Commands that can be run on the context of the target are now run in it. * Simplifies the command registration and execution. * Got rid of the huge switch block. * Introduced AddClientCommand to register new commands for example * `AddClientCommand("SwitchTeams", true, ::TeamSwitchImpl);` * `AddClientCommand("Hide", false, ::HideImpl);` * Callbacks are called with the full event object and the parsed data as parameters to allow maximum flexibility. * Introduced level.eventBus.gamename to know which game we are to add minor changes. * Changes - noclip/lockcontrols/playertome Additional changes to support other games' functions Co-Authored-By: Amos <4959320+MrAmos123@users.noreply.github.com> --- GameFiles/README.MD | 16 + .../userraw/scripts => }/_integration.gsc | 349 ++++++++++++++---- IW4MAdmin.sln | 2 +- Plugins/ScriptPlugins/GameInterface.js | 108 +++++- 4 files changed, 398 insertions(+), 77 deletions(-) create mode 100644 GameFiles/README.MD rename GameFiles/{IW4x/userraw/scripts => }/_integration.gsc (69%) diff --git a/GameFiles/README.MD b/GameFiles/README.MD new file mode 100644 index 000000000..3eafed850 --- /dev/null +++ b/GameFiles/README.MD @@ -0,0 +1,16 @@ +# Game Interface + +Allows integration of IW4M-Admin to GSC, mainly used for special commands that need to use GSC in order to work. +But can also be used to read / write metadata from / to a profile and to get the player permission level. + + +## Installation Plutonium IW5 + + +Move `_integration.gsc` to `%localappdata%\Plutonium\storage\iw5\scripts\` + + +## Installation IW4x + + +Move `_integration.gsc` to `IW4x/userraw/scripts`, `IW4x` being the root folder of your game server. \ No newline at end of file diff --git a/GameFiles/IW4x/userraw/scripts/_integration.gsc b/GameFiles/_integration.gsc similarity index 69% rename from GameFiles/IW4x/userraw/scripts/_integration.gsc rename to GameFiles/_integration.gsc index b06fedf5d..b6f2237ff 100644 --- a/GameFiles/IW4x/userraw/scripts/_integration.gsc +++ b/GameFiles/_integration.gsc @@ -1,7 +1,6 @@ #include common_scripts\utility; #include maps\mp\_utility; #include maps\mp\gametypes\_hud_util; -#include maps\mp\gametypes\_playerlogic; init() { @@ -12,6 +11,7 @@ init() level.eventBus.failKey = "fail"; level.eventBus.timeoutKey = "timeout"; level.eventBus.timeout = 30; + level.eventBus.gamename = getDvar( "gamename" ); // We want to do a few small detail different on IW5 compared to IW4, nothing where 2 files would make sense. level.clientDataKey = "clientData"; @@ -35,18 +35,26 @@ init() level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived; level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand; level.eventCallbacks[level.eventTypes.setClientDataCompleted] = ::OnSetClientDataCompleted; + + level.clientCommandCallbacks = []; + level.clientCommandRusAsTarget = []; if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 ) { return; } + InitializeGameMethods(); + RegisterClientCommands(); + // start long running tasks level thread MonitorClientEvents(); level thread MonitorBus(); level thread OnPlayerConnect(); } + + ////////////////////////////////// // Client Methods ////////////////////////////////// @@ -61,7 +69,7 @@ OnPlayerConnect() level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" ); - if ( player.pers["isBot"] ) + if ( isDefined(player.pers["isBot"]) && player.pers["isBot"] ) { // we don't want to track bots continue; @@ -109,26 +117,26 @@ OnPlayerDisconnect() OnPlayerJoinedTeam() { - self endon( "disconnect" ); + self endon( "disconnect" ); - for( ;; ) - { - self waittill( "joined_team" ); + 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" ); + self endon( "disconnect" ); - for( ;; ) - { + for( ;; ) + { self waittill( "joined_spectators" ); LogPrint( GenerateJoinTeamString( true ) ); - } + } } OnGameEnded() @@ -209,7 +217,7 @@ MonitorClientEvents() if ( level.iw4adminIntegrationDebug == 1 ) { - self IPrintLn( "Processing Event " + client.event.type + "-" + client.event.subtype ); + IPrintLn( "Processing Event " + client.event.type + "-" + client.event.subtype ); } eventHandler = level.eventCallbacks[client.event.type]; @@ -227,6 +235,53 @@ MonitorClientEvents() // Helper Methods ////////////////////////////////// +RegisterClientCommands() +{ + AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl ); + AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl ); + AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl ); + AddClientCommand( "Hide", false, ::HideImpl ); + AddClientCommand( "Unhide", false, ::UnhideImpl ); + AddClientCommand( "Alert", true, ::AlertImpl ); + AddClientCommand( "Goto", false, ::GotoImpl ); + AddClientCommand( "Kill", true, ::KillImpl ); + AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl ); + AddClientCommand( "NightMode", false, ::NightModeImpl ); //This really should be a level command + AddClientCommand( "LockControls", true, ::LockControlsImpl ); + AddClientCommand( "UnlockControls", true, ::UnlockControlsImpl ); + AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl ); + AddClientCommand( "NoClip", false, ::NoClipImpl ); + AddClientCommand( "NoClipOff", false, ::NoClipOffImpl ); +} + +InitializeGameMethods() +{ + level.overrideMethods = []; + level.overrideMethods["god"] = ::_god; + level.overrideMethods["noclip"] = ::UnsupportedFunc; + + if ( isDefined( ::God ) ) + { + level.overrideMethods["god"] = ::God; + } + + if ( isDefined( ::NoClip ) ) + { + level.overrideMethods["noclip"] = ::NoClip; + } + + if ( level.eventBus.gamename == "IW5" ) + { //PlutoIW5 only allows Godmode and NoClip if cheats are on.. + level.overrideMethods["god"] = ::IW5_God; + level.overrideMethods["noclip"] = ::IW5_NoClip; + } +} + +UnsupportedFunc() +{ + self IPrintLnBold( "Function is not supported!" ); +} + RequestClientMeta( metaKey ) { getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey ); @@ -490,14 +545,65 @@ NotifyClientEvent( eventInfo ) if ( level.iw4adminIntegrationDebug == 1 ) { IPrintLn( "NotifyClientEvent->" + event.data ); + if( int( eventInfo[3] ) != -1 && !isDefined( origin ) ) + { + IPrintLn( "origin is null but the slot id is " + int( eventInfo[3] ) ); + } + if( int( eventInfo[4] ) != -1 && !isDefined( target ) ) + { + IPrintLn( "target is null but the slot id is " + int( eventInfo[4] ) ); + } } - client = event.origin; + if( isDefined( target ) ) + { + client = event.target; + } + else if( isDefined( origin ) ) + { + client = event.origin; + } + else + { + if ( level.iw4adminIntegrationDebug == 1 ) + { + IPrintLn( "Neither origin or target are set but we are a Client Event, aborting" ); + } + + return; + } client.event = event; level notify( level.eventTypes.localClientEvent, client ); } +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; +} + +AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite ) +{ + if ( isDefined( level.clientCommandCallbacks[commandName] ) && isDefined( shouldOverwrite ) && !shouldOverwrite ) { + + return; + } + level.clientCommandCallbacks[commandName] = callback; + level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean +} + + + ////////////////////////////////// // Event Handlers ///////////////////////////////// @@ -544,45 +650,18 @@ OnExecuteCommand( event ) data = ParseDataString( event.data ); response = ""; - switch ( event.subtype ) + command = level.clientCommandCallbacks[event.subtype]; + runAsTarget = level.clientCommandRusAsTarget[event.subtype]; + executionContextEntity = event.origin; + if ( runAsTarget ) { + executionContextEntity = event.target; + } + if ( isDefined( command ) ) { + response = executionContextEntity [[command]]( event, data ); + } + else if ( level.iw4adminIntegrationDebug == 1 ) { - case "GiveWeapon": - response = event.target GiveWeaponImpl( data ); - break; - case "TakeWeapons": - response = event.target TakeWeaponsImpl(); - break; - case "SwitchTeams": - response = event.target TeamSwitchImpl(); - break; - case "Hide": - response = self HideImpl(); - break; - case "Unhide": - response = self UnhideImpl(); - break; - case "Alert": - response = event.target AlertImpl( data ); - break; - case "Goto": - if ( IsDefined( event.target ) ) - { - response = self GotoPlayerImpl( event.target ); - } - else - { - response = self GotoImpl( data ); - } - break; - case "Kill": - response = event.target KillImpl(); - break; - case "NightMode": - NightModeImpl(); - break; - case "SetSpectator": - response = event.target SetSpectatorImpl(); - break; + IPrintLn( "Unkown Client command->" + event.subtype); } // send back the response to the origin, but only if they're not the target @@ -605,7 +684,7 @@ OnSetClientDataCompleted( event ) // Command Implementations ///////////////////////////////// -GiveWeaponImpl( data ) +GiveWeaponImpl( event, data ) { if ( !IsAlive( self ) ) { @@ -636,7 +715,7 @@ TeamSwitchImpl() { if ( !IsAlive( self ) ) { - return self.name + "^7 is not alive"; + return self + "^7 is not alive"; } team = level.allies; @@ -653,6 +732,79 @@ TeamSwitchImpl() return self.name + "^7 switched to " + self.team; } +LockControlsImpl() +{ + if ( !IsAlive( self ) ) + { + return self.name + "^7 is not alive"; + } + + + self freezeControls( true ); + self call [[level.overrideMethods["god"]]]( true ); + self Hide(); + + info = []; + info[ "alertType" ] = "Alert!"; + info[ "message" ] = "You have been frozen!"; + + self AlertImpl( undefined, info ); + + return self.name + "\'s controls are locked"; +} + +UnlockControlsImpl() +{ + if ( !IsAlive( self ) ) + { + return self.name + "^7 is not alive"; + } + + self freezeControls( false ); + self call [[level.overrideMethods["god"]]]( false ); + self Show(); + + return self.name + "\'s controls are unlocked"; +} + +NoClipImpl() +{ + if ( !IsAlive( self ) ) + { + self IPrintLnBold( "You are not alive" ); + return; + } + + self SetClientDvar( "sv_cheats", 1 ); + self SetClientDvar( "cg_thirdperson", 1 ); + self SetClientDvar( "sv_cheats", 0 ); + + self call [[level.overrideMethods["god"]]]( true ); + self call [[level.overrideMethods["noclip"]]]( true ); + self Hide(); + + self IPrintLnBold( "NoClip enabled" ); +} + +NoClipOffImpl() +{ + if ( !IsAlive( self ) ) + { + self IPrintLnBold( "You are not alive" ); + return; + } + + self SetClientDvar( "sv_cheats", 1 ); + self SetClientDvar( "cg_thirdperson", 0 ); + self SetClientDvar( "sv_cheats", 0 ); + + self call [[level.overrideMethods["god"]]]( false ); + self call [[level.overrideMethods["noclip"]]]( false ); + self Show(); + + self IPrintLnBold( "NoClip disabled" ); +} + HideImpl() { if ( !IsAlive( self ) ) @@ -671,10 +823,7 @@ HideImpl() self.savedMaxHealth = self.maxhealth; } - self.maxhealth = 99999; - self.health = 99999; - self.isHidden = true; - + self call [[level.overrideMethods["god"]]]( true ); self Hide(); self IPrintLnBold( "You are now ^5hidden ^7from other players" ); @@ -698,21 +847,36 @@ UnhideImpl() self SetClientDvar( "cg_thirdperson", 0 ); self SetClientDvar( "sv_cheats", 0 ); - self.health = self.savedHealth; - self.maxhealth = self.savedMaxHealth; - self.isHidden = false; - + self call [[level.overrideMethods["god"]]]( false ); self Show(); + self IPrintLnBold( "You are now ^5visible ^7to other players" ); } -AlertImpl( data ) +AlertImpl( event, data ) { - self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 ); + 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 ); + } + if ( level.eventBus.gamename == "IW5" ) { //IW5's notification are a bit different... + 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; } -GotoImpl( data ) +GotoImpl( event, data ) +{ + if ( IsDefined( event.target ) ) + { + return self GotoPlayerImpl( event.target ); + } + else + { + return self GotoCoordImpl( data ); + } +} + +GotoCoordImpl( data ) { if ( !IsAlive( self ) ) { @@ -737,6 +901,18 @@ GotoPlayerImpl( target ) self IPrintLnBold( "Moved to " + target.name ); } +PlayerToMeImpl( event ) +{ + if ( !IsAlive( self ) ) + { + return self.name + " is not alive"; + } + + self SetOrigin( event.origin GetOrigin() ); + return "Moved here " + self.name; +} + + KillImpl() { if ( !IsAlive( self ) ) @@ -805,3 +981,48 @@ SetSpectatorImpl() return self.name + " has been moved to spectator"; } + +////////////////////////////////// +// Function Overrides +////////////////////////////////// + +_god( isEnabled ) +{ + if ( isEnabled == true ) + { + if ( !IsDefined( self.savedHealth ) || self.health < 1000 ) + { + self.savedHealth = self.health; + self.savedMaxHealth = self.maxhealth; + } + + self.maxhealth = 99999; + self.health = 99999; + } + + else + { + if ( !IsDefined( self.savedHealth ) || !IsDefined( self.savedMaxHealth ) ) + { + return; + } + + self.health = self.savedHealth; + self.maxhealth = self.savedMaxHealth; + } +} + + +IW5_God() +{ + SetDvar( "sv_cheats", 1 ); + self God(); + SetDvar( "sv_cheats", 0 ); +} + +IW5_NoClip() +{ + SetDvar( "sv_cheats", 1 ); + self NoClip(); + SetDvar( "sv_cheats", 0 ); +} diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 9ffe10555..b59f4a039 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -13,7 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution version.txt = version.txt DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1 DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh - GameFiles\IW4x\userraw\scripts\_integration.gsc = GameFiles\IW4x\userraw\scripts\_integration.gsc + GameFiles\_integration.gsc = GameFiles\_integration.gsc EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}" diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 415085c5c..f408dc140 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -85,7 +85,7 @@ let commands = [{ name: 'weapon name', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -103,7 +103,7 @@ let commands = [{ name: 'player', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -121,7 +121,7 @@ let commands = [{ name: 'player', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -129,6 +129,72 @@ let commands = [{ sendScriptCommand(gameEvent.Owner, 'SwitchTeams', gameEvent.Origin, gameEvent.Target, undefined); } }, + { + name: 'lockcontrols', + description: 'locks target player\'s controls', + alias: 'lc', + permission: 'Administrator', + targetRequired: true, + arguments: [{ + name: 'player', + required: true + }], + supportedGames: ['IW4', 'IW5'], + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { + return; + } + sendScriptCommand(gameEvent.Owner, 'LockControls', gameEvent.Origin, gameEvent.Target, undefined); + } + }, + { + name: 'unlockcontrols', + description: 'unlocks target player\'s controls', + alias: 'ulc', + permission: 'Administrator', + targetRequired: true, + arguments: [{ + name: 'player', + required: true + }], + supportedGames: ['IW4', 'IW5'], + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { + return; + } + sendScriptCommand(gameEvent.Owner, 'UnlockControls', gameEvent.Origin, gameEvent.Target, undefined); + } + }, + { + name: 'noclip', + description: 'enable noclip on yourself ingame', + alias: 'nc', + permission: 'SeniorAdmin', + targetRequired: false, + arguments: [], + supportedGames: ['IW4', 'IW5'], + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { + return; + } + sendScriptCommand(gameEvent.Owner, 'NoClip', gameEvent.Origin, gameEvent.Origin, undefined); + } + }, + { + name: 'noclipoff', + description: 'disable noclip on yourself ingame', + alias: 'nco', + permission: 'SeniorAdmin', + targetRequired: false, + arguments: [], + supportedGames: ['IW4', 'IW5'], + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { + return; + } + sendScriptCommand(gameEvent.Owner, 'NoClipOff', gameEvent.Origin, gameEvent.Origin, undefined); + } + }, { name: 'hide', description: 'hide yourself ingame', @@ -136,7 +202,7 @@ let commands = [{ permission: 'SeniorAdmin', targetRequired: false, arguments: [], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -151,7 +217,7 @@ let commands = [{ permission: 'SeniorAdmin', targetRequired: false, arguments: [], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -173,7 +239,7 @@ let commands = [{ name: 'message', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -194,7 +260,7 @@ let commands = [{ name: 'player', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -202,6 +268,24 @@ let commands = [{ sendScriptCommand(gameEvent.Owner, 'Goto', gameEvent.Origin, gameEvent.Target, undefined); } }, + { + name: 'playertome', + description: 'teleport a player to you', + alias: 'p2m', + permission: 'SeniorAdmin', + targetRequired: true, + arguments: [{ + name: 'player', + required: true + }], + supportedGames: ['IW4', 'IW5'], + execute: (gameEvent) => { + if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { + return; + } + sendScriptCommand(gameEvent.Owner, 'PlayerToMe', gameEvent.Origin, gameEvent.Target, undefined); + } + }, { name: 'goto', description: 'teleport to a position', @@ -220,7 +304,7 @@ let commands = [{ name: 'z', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -244,7 +328,7 @@ let commands = [{ name: 'player', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -259,7 +343,7 @@ let commands = [{ permission: 'SeniorAdmin', targetRequired: false, arguments: [], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -277,7 +361,7 @@ let commands = [{ name: 'player', required: true }], - supportedGames: ['IW4'], + supportedGames: ['IW4', 'IW5'], execute: (gameEvent) => { if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) { return; @@ -489,7 +573,7 @@ const pollForEvents = server => { const nextMessage = state.queuedMessages.splice(0, 1); setDvar(server, outDvar, nextMessage, onSetDvar); } - + if (state.waitingOnOutput) { getDvar(server, outDvar, onReceivedDvar); }