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 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/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); } diff --git a/Application/Plugin/Script/ScriptPluginHelper.cs b/Application/Plugin/Script/ScriptPluginHelper.cs index be15c580a..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 { @@ -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/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml index 67bdc3b96..e8463b150 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,233 @@ 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_Deploy + 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: or(eq(variables['Build.SourceBranch'], 'refs/heads/release/pre'), eq(variables['Build.SourceBranch'], 'refs/heads/develop')) + 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' - } - - Invoke-RestMethod @params + - 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' + + - 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: PublishPipelineArtifact@1 - displayName: 'Publish artifact for analysis' - inputs: - targetPath: '$(outputFolder)' - artifact: 'IW4MAdmin.$(buildConfiguration)' - publishLocation: 'pipeline' + - 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: FtpUpload@2 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') + 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 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') + 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 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') + 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: 'Automated rolling release - changelog below. [Updating Instructions](https://github.com/RaidMax/IW4M-Admin/wiki/Getting-Started#updating)' + changeLogCompareToRelease: 'lastNonDraftRelease' + changeLogType: 'commitBased' + + - task: PowerShell@2 + condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') + 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 diff --git a/GameFiles/GameInterface/_integration_base.gsc b/GameFiles/GameInterface/_integration_base.gsc index fac8a6688..dffb68c5b 100644 --- a/GameFiles/GameInterface/_integration_base.gsc +++ b/GameFiles/GameInterface/_integration_base.gsc @@ -19,11 +19,33 @@ 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.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.eventBus.inLocation = ""; + level.eventBus.outLocation = ""; level.notifyTypes = spawnstruct(); level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized"; @@ -33,7 +55,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"; @@ -51,12 +73,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,122 +86,58 @@ Setup() _SetDvarIfUninitialized( level.eventBus.inVar, "" ); _SetDvarIfUninitialized( level.eventBus.outVar, "" ); - _SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 ); + _SetDvarIfUninitialized( level.commonKeys.enabled, 1 ); + _SetDvarIfUninitialized( level.commonKeys.busMode, "rcon" ); + _SetDvarIfUninitialized( level.commonKeys.busdir, "" ); _SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 ); + _SetDvarIfUninitialized( "GroupSeparatorChar", "" ); + _SetDvarIfUninitialized( "RecordSeparatorChar", "" ); + _SetDvarIfUninitialized( "UnitSeparatorChar", "" ); - 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 MonitorEvents(); + thread MonitorBus(); } -////////////////////////////////// -// Client Methods -////////////////////////////////// - -OnPlayerConnect() +MonitorEvents() { - 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 ( ;; ) { - 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 ); + 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 = []; } } @@ -188,11 +145,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 +159,42 @@ _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; +} + +_GetInboundData( location ) +{ + return GetDvar( level.eventBus.inVar ); +} + +_GetOutboundData( location ) +{ + return GetDvar( level.eventBus.outVar ); +} + +_SetInboundData( location, data ) +{ + return SetDvar( level.eventBus.inVar, data ); +} + +_SetOutboundData( location, data ) +{ + return SetDvar( level.eventBus.outVar, data ); } // Not every game can output to console or even game log. @@ -223,7 +215,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 ) ); } } @@ -285,13 +277,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 ) @@ -306,18 +298,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 ) ) @@ -326,7 +320,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 ) @@ -341,6 +335,11 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) eventSubtype = "None"; } + if ( !IsDefined( entOrId ) ) + { + entOrId = "-1"; + } + if ( IsPlayer( entOrId ) ) { entOrId = entOrId getEntityNumber(); @@ -352,52 +351,65 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) { request = "1"; } - - request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data; + + data = BuildDataString( data ); + groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); + request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data; + return request; } MonitorBus() { - level endon( "game_ended" ); + level endon( level.eventTypes.gameEnd ); + + 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 - if ( getDvar( level.eventBus.inVar ) == "" ) + inVal = [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ); + + if ( !IsDefined( inVal ) || inVal == "" ) { level notify( "bus_ready" ); } - eventString = getDvar( level.eventBus.outVar ); + eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]]( level.eventBus.outLocation ); - if ( eventString == "" ) + if ( !IsDefined( eventString ) || eventString == "" ) { continue; } + LogDebug( "-> " + eventString ); - NotifyClientEvent( strtok( eventString, ";" ) ); + groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 ); + NotifyEvent( strtok( eventString, groupSeparator ) ); - SetDvar( level.eventBus.outVar, "" ); + [[level.busMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" ); } } QueueEvent( request, eventType, notifyEntity ) { - level endon( "game_ended" ); + level endon( level.eventTypes.gameEnd ); start = GetTime(); maxWait = level.eventBus.timeout * 1000; // 30 seconds timedOut = ""; - while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait ) + while ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" && ( 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 ) != "" ) + if ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" ) { LogDebug( "A request is already in progress..." ); timedOut = "set"; @@ -407,7 +419,7 @@ QueueEvent( request, eventType, notifyEntity ) timedOut = "unset"; } - if ( timedOut == "set") + if ( timedOut == "set" ) { LogDebug( "Timed out waiting for response..." ); @@ -416,14 +428,14 @@ QueueEvent( request, eventType, notifyEntity ) notifyEntity NotifyClientEventTimeout( eventType ); } - SetDvar( level.eventBus.inVar, "" ); + [[level.busMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" ); return; } - LogDebug("<- " + request ); + LogDebug( "<- " + request ); - SetDvar( level.eventBus.inVar, request ); + [[level.busMethods[level.commonFunctions.setInboundData]]]( level.eventBus.inLocation, request ); } ParseDataString( data ) @@ -434,13 +446,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; @@ -450,6 +462,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 @@ -459,7 +491,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] ) ); @@ -467,15 +499,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] ) ); @@ -485,23 +512,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 ) @@ -521,7 +540,6 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite ) OnClientDataReceived( event ) { - event.data = ParseDataString( event.data ); clientData = self.pers[level.clientDataKey]; if ( event.subtype == "Fail" ) @@ -541,7 +559,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,13 +571,11 @@ OnClientDataReceived( event ) clientData.performance = event.data["performance"]; clientData.state = "complete"; self.persistentClientId = event.data["clientId"]; - - self thread DisplayWelcomeData(); } OnExecuteCommand( event ) { - data = ParseDataString( event.data ); + data = event.data; response = ""; command = level.clientCommandCallbacks[event.subtype]; @@ -573,7 +589,14 @@ OnExecuteCommand( event ) if ( IsDefined( command ) ) { - response = executionContextEntity [[command]]( event, data ); + if ( IsDefined( executionContextEntity ) ) + { + response = executionContextEntity thread [[command]]( event, data ); + } + else + { + thread [[command]]( event ); + } } else { @@ -589,6 +612,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..9a717d0eb 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,23 @@ 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; + + 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(); - _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,12 +54,12 @@ OnPlayerConnect() { level waittill( "connected", player ); - if ( player call [[ level.overrideMethods[ level.commonFunctions.isBot ] ]]() ) + if ( player IsTestClient() ) { // we don't want to track bots - continue; + continue; } - + player thread SetPersistentData(); player thread WaitForClientEvents(); } @@ -87,7 +90,7 @@ WaitForClientEvents() for ( ;; ) { - self waittill( level.eventTypes.localClientEvent, event ); + self waittill( level.eventTypes.eventAvailable, event ); scripts\_integration_base::LogDebug( "Received client event " + event.type ); @@ -99,6 +102,26 @@ WaitForClientEvents() } } +GetInboundData( location ) +{ + return FileRead( location ); +} + +GetOutboundData( location ) +{ + return FileRead( location ); +} + +SetInboundData( location, data ) +{ + FileWrite( location, data, "write" ); +} + +SetOutboundData( location, data ) +{ + FileWrite( location, data, "write" ); +} + GetMaxClients() { return level.maxClients; @@ -186,12 +209,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 +219,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 +547,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..4619656cb 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; + level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; 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,140 +61,19 @@ Log2Console( logLevel, message ) Print( "[" + logLevel + "] " + message + "\n" ); } -_GetXUID() +IsBotWrapper( client ) +{ + return client IsTestClient(); +} + +GetXuidWrapper() { return self GetXUID(); } -////////////////////////////////// -// GUID helpers -///////////////////////////////// - -SetPersistentData() +WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 ) { - 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; - } + return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ); } ////////////////////////////////// @@ -427,10 +256,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..fc2b30073 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,30 +30,52 @@ 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"; + + level.eventTypes.urlRequested = "UrlRequested"; + 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.getBusModeRequested] = ::OnBusModeRequestedCallback; level.iw4madminIntegrationDefaultPerformance = 200; + level.notifyEntities = []; + level.customCommands = []; 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 +86,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 +115,341 @@ 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 + " ago" ); +} + + +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 ); +} + +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" ); + + 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 ) +{ + 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 ); +} + +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 ); + } + + level.customCommands[level.customCommands.size] = data; +} + +// #end region + +// #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" ); + + 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; + } + } + + return max; +} + +// #end region + +// #region team balance + +OnPlayerDisconnect() { level endon( level.eventTypes.gameEnd ); self endon( "disconnect_logic_end" ); @@ -106,7 +464,7 @@ OnClientDisconnect() } } -OnClientJoinedTeam() +OnPlayerJoinedTeam() { self endon( level.eventTypes.disconnect ); @@ -114,6 +472,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; @@ -126,7 +492,7 @@ OnClientJoinedTeam() 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; } @@ -141,12 +507,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; } @@ -341,7 +729,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; } @@ -467,48 +855,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 +886,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..d56821f12 100644 --- a/GameFiles/GameInterface/_integration_t5zm.gsc +++ b/GameFiles/GameInterface/_integration_t5zm.gsc @@ -7,50 +7,25 @@ Init() Setup() { - level endon( "game_ended" ); + level endon( "end_game" ); + 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"; + level.eventTypes.gameEnd = "end_game"; 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 +43,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 +72,6 @@ Log2Console( logLevel, message ) God() { - if ( !IsDefined( self.godmode ) ) { self.godmode = false; @@ -137,6 +89,16 @@ God() } } +IsBotWrapper( client ) +{ + return ( IsDefined ( client.pers["isBot"] ) && client.pers["isBot"] != 0 ); +} + +GetXuidWrapper() +{ + return self GetXUID(); +} + _GetPlayerFromClientNum( clientNum ) { if ( clientNum < 0 ) @@ -144,11 +106,12 @@ _GetPlayerFromClientNum( clientNum ) return undefined; } - players = GetPlayers("all"); + players = GetPlayers( "all" ); 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 +121,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 +304,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..e049b9cc4 100644 --- a/GameFiles/GameInterface/_integration_t6.gsc +++ b/GameFiles/GameInterface/_integration_t6.gsc @@ -9,49 +9,29 @@ Init() Setup() { level endon( "game_ended" ); + level endon( "end_game" ); + 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"; + + if ( sessionmodeiszombiesgame() ) + { + level.eventTypes.gameEnd = "end_game"; + } 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; + level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; 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 +49,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 +72,6 @@ Log2Console( logLevel, message ) God() { - if ( !IsDefined( self.godmode ) ) { self.godmode = false; @@ -132,142 +89,21 @@ God() } } -_GetXUID() +IsBotWrapper( client ) +{ + return client maps\mp\_utility::is_bot(); +} + +GetXuidWrapper() { return self GetXUID(); } -////////////////////////////////// -// GUID helpers -///////////////////////////////// - -/*SetPersistentData() +WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 ) { - 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"] ); + return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ); } -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 +292,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; } @@ -482,7 +308,7 @@ GotoImpl( event, data ) } } -GotoCoordImpl( event, data ) +GotoCoordImpl( data ) { if ( !IsAlive( self ) ) { @@ -550,7 +376,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 +393,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..befcf0a29 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 +} diff --git a/GameFiles/GameInterface/example_module.gsc b/GameFiles/GameInterface/example_module.gsc new file mode 100644 index 000000000..4dc8cceb8 --- /dev/null +++ b/GameFiles/GameInterface/example_module.gsc @@ -0,0 +1,88 @@ +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, "\"" ); + + if ( IsPlayer( self ) ) + { + self IPrintLnBold ( "^5" + parsedResponse[parsedResponse.size - 2] ); + } +} diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 7d0cae409..1b05d620f 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,10 @@ 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 + 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 5fd82d993..19cf64dbd 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -2,34 +2,61 @@ 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) => { +let busFileIn = ''; +let busFileOut = ''; +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)); 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); + 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, + configWrapper: null, + config: { + pollingRate: 300 + }, - onLoad: function (serviceResolver, config) { + 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) { @@ -65,6 +92,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; @@ -87,15 +117,22 @@ 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'); // loop restarts this.requestGetDvar(inDvar, serverValueEvent.server); }, + + onServerMonitoringStart: function (monitorStartEvent) { + this.initializeServer(monitorStartEvent.server); + }, + + onMatchStart: function (matchStartEvent) { + busMode = 'rcon'; + this.sendEventMessage(matchStartEvent.server, true, 'GetBusModeRequested', null, null, null, {}); + }, initializeServer: function (server) { servers[server.id] = { @@ -125,7 +162,12 @@ 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, {}); this.requestGetDvar(inDvar, responseEvent.server); }, @@ -136,7 +178,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; @@ -179,8 +223,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'); @@ -208,7 +252,7 @@ const plugin = { data = { level: client.level, clientId: client.clientId, - lastConnection: client.lastConnection, + lastConnection: client.timeSinceLastConnectionString, tag: tagMeta?.value ?? '', performance: clientStats?.performance ?? 200.0 }; @@ -287,17 +331,74 @@ 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); + + if (typeof response !== 'string' && !(response instanceof String)) { + response = JSON.stringify(response); + } + + const max = 10; + this.logger.logDebug(`response length ${response.length}`); + + 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!`); + 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]}); + } + }); + } + + 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.replace('\'', '').replace('"', ''); + if (event.data?.inLocation && event.data?.outLocation) { + busFileIn = event.data?.inLocation; + busFileOut = event.data?.outLocation; + } + this.logger.logDebug('Setting bus mode to {mode} {dir}', busMode, busDir); + } + } + 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); @@ -305,9 +406,40 @@ 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}/${fileForDvar(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; + requestEvent.delayMs = this.config.pollingRate; requestEvent.timeoutMs = 2000; requestEvent.source = this.name; @@ -317,7 +449,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); } } @@ -335,10 +467,39 @@ 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}/${fileForDvar(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); - requestEvent.delayMs = pollingRate; + requestEvent.delayMs = this.config.pollingRate; requestEvent.timeoutMs = 2000; requestEvent.source = this.name; @@ -348,7 +509,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); } } @@ -365,8 +526,64 @@ 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); + }, + + 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; + } + + 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, { + args: gameEvent.data + }); + } + } + }] + } + + this.scriptHelper.registerDynamicCommand(commandWrapper); } }; @@ -634,7 +851,7 @@ const parseEvent = (input) => { return {}; } - const eventInfo = input.split(';'); + const eventInfo = input.split(groupSeparatorChar); return { eventType: eventInfo[1], @@ -652,7 +869,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 +881,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 +906,20 @@ 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; +} + +const fileForDvar = (dvar) => { + if (dvar === inDvar) { + return busFileIn; + } + + return busFileOut; +} 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