Compare commits

..

9 Commits

117 changed files with 3522 additions and 7047 deletions

View File

@ -55,7 +55,7 @@ public class AlertManager : IAlertManager
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt).ToList();
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
finally
{

View File

@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2049" />
<PackageReference Include="Jint" Version="3.0.0-beta-2047" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>
@ -32,13 +32,11 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion>

View File

@ -581,9 +581,9 @@ namespace IW4MAdmin.Application
throw lastException;
}
if (successServers != config.Servers.Length && !AppContext.TryGetSwitch("NoConfirmPrompt", out _))
if (successServers != config.Servers.Length)
{
if (!Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"].PromptBool())
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}

View File

@ -1,80 +0,0 @@
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using Serilog.Core;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class SetLogLevelCommand : Command
{
private readonly Func<string, LoggingLevelSwitch> _levelSwitchResolver;
public SetLogLevelCommand(CommandConfiguration config, ITranslationLookup layout, Func<string, LoggingLevelSwitch> levelSwitchResolver) : base(config, layout)
{
_levelSwitchResolver = levelSwitchResolver;
Name = "loglevel";
Alias = "ll";
Description = "set minimum logging level";
Permission = EFClient.Permission.Owner;
Arguments = new CommandArgument[]
{
new()
{
Name = "Log Level",
Required = true
},
new()
{
Name = "Override",
Required = false
},
new()
{
Name = "IsDevelopment",
Required = false
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var args = gameEvent.Data.Split(" ");
if (!Enum.TryParse<LogEventLevel>(args[0], out var minLevel))
{
await gameEvent.Origin.TellAsync(new[]
{
$"Valid log values: {string.Join(",", Enum.GetValues<LogEventLevel>())}"
});
return;
}
var context = string.Empty;
if (args.Length > 1)
{
context = args[1];
}
var loggingSwitch = _levelSwitchResolver(context);
loggingSwitch.MinimumLevel = minLevel;
if (args.Length > 2 && (args[2] == "1" || args[2].ToLower() == "true"))
{
AppContext.SetSwitch("IsDevelop", true);
}
else
{
AppContext.SetSwitch("IsDevelop", false);
}
await gameEvent.Origin.TellAsync(new[]
{ $"Set minimum log level to {loggingSwitch.MinimumLevel.ToString()}" });
}
}

View File

@ -311,10 +311,6 @@
{
"Name": "tdm",
"Alias": "Team Deathmatch"
},
{
"Name": "zom",
"Alias": "Zombies"
}
]
},
@ -1234,47 +1230,7 @@
{
"Alias": "Zoo",
"Name": "mp_zoo"
},
{
"Alias": "Kino der Toten",
"Name": "zombie_theater"
},
{
"Alias": "Five",
"Name": "zombie_pentagon"
},
{
"Alias": "Ascension",
"Name": "zombie_cosmodrome"
},
{
"Alias": "Call of the Dead",
"Name": "zombie_coast"
},
{
"Alias": "Shangri-La",
"Name": "zombie_temple"
},
{
"Alias": "Moon",
"Name": "zombie_moon"
},
{
"Alias": "Nacht Der Untoten",
"Name": "zombie_cod5_prototype"
},
{
"Alias": "Verrückt",
"Name": "zombie_cod5_asylum"
},
{
"Alias": "Shi No Numa",
"Name": "zombie_cod5_sumpf"
},
{
"Alias": "Der Riese",
"Name": "zombie_cod5_factory"
}
}
]
},
{

View File

@ -8,7 +8,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
@ -18,10 +17,7 @@ namespace IW4MAdmin.Application.Extensions
{
public static class StartupExtensions
{
private static ILogger _defaultLogger;
private static readonly LoggingLevelSwitch LevelSwitch = new();
private static readonly LoggingLevelSwitch MicrosoftLevelSwitch = new();
private static readonly LoggingLevelSwitch SystemLevelSwitch = new();
private static ILogger _defaultLogger = null;
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
ApplicationConfiguration appConfig)
@ -33,37 +29,21 @@ namespace IW4MAdmin.Application.Extensions
.Build();
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration);
LevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Default"]);
MicrosoftLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:Microsoft"]);
SystemLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:System"]);
loggerConfig = loggerConfig.MinimumLevel.ControlledBy(LevelSwitch);
loggerConfig = loggerConfig.MinimumLevel.Override("Microsoft", MicrosoftLevelSwitch)
.MinimumLevel.Override("System", SystemLevelSwitch);
.ReadFrom.Configuration(configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
if (Utilities.IsDevelopment)
{
loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Debug();
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Debug();
}
_defaultLogger = loggerConfig.CreateLogger();
}
services.AddSingleton((string context) =>
{
return context.ToLower() switch
{
"microsoft" => MicrosoftLevelSwitch,
"system" => SystemLevelSwitch,
_ => LevelSwitch
};
});
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
services.AddSingleton(new LoggerFactory()
.AddSerilog(_defaultLogger, true));

View File

@ -129,7 +129,7 @@ public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHand
await _onIo.WaitAsync();
}
await using var fileStream = File.Create(_path);
await using var fileStream = File.OpenWrite(_path);
await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions);
await fileStream.DisposeAsync();
_configurationInstance = configuration;

View File

@ -377,6 +377,7 @@ 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()
@ -539,7 +540,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.GameName, E.Target.CurrentAlias?.IPAddress, new[] {EFPenalty.PenaltyType.Flag});
E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty);
Manager.QueueEvent(new ClientPenaltyRevokeEvent

View File

@ -58,30 +58,9 @@ namespace IW4MAdmin.Application
/// entrypoint of the application
/// </summary>
/// <returns></returns>
public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 25, int? requestQueueLimit = 25)
public static async Task Main(string[] args)
{
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
var libraryName = eventArgs.Name.Split(",").First();
var overrides = new[] { nameof(SharedLibraryCore), nameof(Stats) };
if (!overrides.Contains(libraryName))
{
return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(asm => asm.FullName == eventArgs.Name);
}
// added to be a bit more permissive with plugin references
return AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName?.StartsWith(libraryName) ?? false);
};
if (noConfirm)
{
AppContext.SetSwitch("NoConfirmPrompt", true);
}
Environment.SetEnvironmentVariable("MaxConcurrentRequests", (maxConcurrentRequests * Environment.ProcessorCount).ToString());
Environment.SetEnvironmentVariable("RequestQueueLimit", requestQueueLimit.ToString());
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
@ -94,7 +73,7 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
await LaunchAsync();
await LaunchAsync(args);
}
/// <summary>
@ -120,13 +99,13 @@ namespace IW4MAdmin.Application
/// task that initializes application and starts the application monitoring and runtime tasks
/// </summary>
/// <returns></returns>
private static async Task LaunchAsync()
private static async Task LaunchAsync(string[] args)
{
restart:
ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version}", Version);
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
try
{
@ -432,12 +411,7 @@ namespace IW4MAdmin.Application
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
if (appConfigHandler.Configuration()?.MasterUrl == new Uri("http://api.raidmax.org:5000"))
{
appConfigHandler.Configuration().MasterUrl = new ApplicationConfiguration().MasterUrl;
}
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")

View File

@ -13,10 +13,10 @@ namespace IW4MAdmin.Application.Misc
{
public class RemoteAssemblyHandler : IRemoteAssemblyHandler
{
private const int KeyLength = 32;
private const int TagLength = 16;
private const int NonceLength = 12;
private const int IterationCount = 10000;
private const int keyLength = 32;
private const int tagLength = 16;
private const int nonceLength = 12;
private const int iterationCount = 10000;
private readonly ApplicationConfiguration _appconfig;
private readonly ILogger _logger;
@ -30,7 +30,7 @@ namespace IW4MAdmin.Application.Misc
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
{
return DecryptContent(encryptedAssemblies)
.Select(Assembly.Load);
.Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
}
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
@ -38,24 +38,24 @@ namespace IW4MAdmin.Application.Misc
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
}
private IEnumerable<byte[]> DecryptContent(string[] content)
private byte[][] DecryptContent(string[] content)
{
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
{
_logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
return Array.Empty<byte[]>();
return new byte[0][];
}
var assemblies = content.Select(piece =>
{
var byteContent = Convert.FromBase64String(piece);
var encryptedContent = byteContent.Take(byteContent.Length - (TagLength + NonceLength)).ToArray();
var tag = byteContent.Skip(byteContent.Length - (TagLength + NonceLength)).Take(TagLength).ToArray();
var nonce = byteContent.Skip(byteContent.Length - NonceLength).Take(NonceLength).ToArray();
var decryptedContent = new byte[encryptedContent.Length];
byte[] byteContent = Convert.FromBase64String(piece);
byte[] encryptedContent = byteContent.Take(byteContent.Length - (tagLength + nonceLength)).ToArray();
byte[] tag = byteContent.Skip(byteContent.Length - (tagLength + nonceLength)).Take(tagLength).ToArray();
byte[] nonce = byteContent.Skip(byteContent.Length - nonceLength).Take(nonceLength).ToArray();
byte[] decryptedContent = new byte[encryptedContent.Length];
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(KeyLength));
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id.ToString()), iterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(keyLength));
try
{

View File

@ -20,21 +20,13 @@ namespace IW4MAdmin.Application.Plugin
public class PluginImporter : IPluginImporter
{
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private const string PluginDir = "Plugins";
private static readonly string PluginDir = "Plugins";
private const string PluginV2Match = "^ *((?:var|const|let) +init)|function init";
private readonly ILogger _logger;
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
private static readonly Type[] FilterTypes =
{
typeof(IPlugin),
typeof(IPluginV2),
typeof(Command),
typeof(IBaseConfiguration)
};
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi,
IRemoteAssemblyHandler remoteAssemblyHandler)
{
@ -85,80 +77,74 @@ namespace IW4MAdmin.Application.Plugin
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
var pluginTypes = new List<Type>();
var commandTypes = new List<Type>();
var configurationTypes = new List<Type>();
var pluginTypes = Enumerable.Empty<Type>();
var commandTypes = Enumerable.Empty<Type>();
var configurationTypes = Enumerable.Empty<Type>();
if (!Directory.Exists(pluginDir))
if (Directory.Exists(pluginDir))
{
return (pluginTypes, commandTypes, configurationTypes);
}
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length);
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length);
if (!dllFileNames.Any())
{
return (pluginTypes, commandTypes, configurationTypes);
}
// we only want to load the most recent assembly in case of duplicates
var assemblies = dllFileNames.Select(Assembly.LoadFrom)
.Union(GetRemoteAssemblies())
.GroupBy(assembly => assembly.FullName).Select(assembly =>
assembly.OrderByDescending(asm => asm.GetName().Version).First());
var eligibleAssemblyTypes = assemblies
.SelectMany(asm =>
if (dllFileNames.Length > 0)
{
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
}).Where(type =>
FilterTypes.Any(filterType => type.GetInterface(filterType.Name, false) != null) ||
(type.IsClass && FilterTypes.Contains(type.BaseType)));
foreach (var assemblyType in eligibleAssemblyTypes)
{
var isPlugin =
(assemblyType.GetInterface(nameof(IPlugin), false) ??
assemblyType.GetInterface(nameof(IPluginV2), false)) != null &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
// we only want to load the most recent assembly in case of duplicates
var assemblies = dllFileNames.Select(name => Assembly.LoadFrom(name))
.Union(GetRemoteAssemblies())
.GroupBy(assembly => assembly.FullName).Select(assembly => assembly.OrderByDescending(asm => asm.GetName().Version).First());
if (isPlugin)
{
pluginTypes.Add(assemblyType);
continue;
}
pluginTypes = assemblies
.SelectMany(asm =>
{
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(assemblyType => (assemblyType.GetInterface(nameof(IPlugin), false) ?? assemblyType.GetInterface(nameof(IPluginV2), false)) != null)
.Where(assemblyType => !assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
var isCommand = assemblyType.IsClass && assemblyType.BaseType == typeof(Command) &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
if (isCommand)
{
commandTypes.Add(assemblyType);
continue;
}
commandTypes = assemblies
.SelectMany(asm =>{
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(assemblyType => assemblyType.IsClass && assemblyType.BaseType == typeof(Command))
.Where(assemblyType => !assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
var isConfiguration = assemblyType.IsClass &&
assemblyType.GetInterface(nameof(IBaseConfiguration), false) != null &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
_logger.LogDebug("Discovered {Count} plugin commands", commandTypes.Count());
if (isConfiguration)
{
configurationTypes.Add(assemblyType);
configurationTypes = assemblies
.SelectMany(asm => {
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(asmType =>
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null)
.Where(assemblyType => !assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
_logger.LogDebug("Discovered {Count} configuration implementations", configurationTypes.Count());
}
}
_logger.LogDebug("Discovered {Count} plugin 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);
}
@ -166,11 +152,10 @@ namespace IW4MAdmin.Application.Plugin
{
try
{
_pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription
.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
@ -184,11 +169,9 @@ namespace IW4MAdmin.Application.Plugin
{
try
{
_pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
_pluginSubscription ??= _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription
.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)

View File

@ -14,8 +14,8 @@ public class ScriptPluginHelper
{
private readonly IManager _manager;
private readonly ScriptPluginV2 _scriptPlugin;
private readonly SemaphoreSlim _onRequestRunning = new(1, 1);
private const int RequestTimeout = 5000;
private readonly SemaphoreSlim _onRequestRunning = new(1, 5);
private const int RequestTimeout = 500;
public ScriptPluginHelper(IManager manager, ScriptPluginV2 scriptPlugin)
{
@ -67,7 +67,7 @@ public class ScriptPluginHelper
try
{
await Task.Delay(delayMs, _manager.CancellationToken);
_scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }));
_scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined));
}
catch
{
@ -76,11 +76,6 @@ public class ScriptPluginHelper
});
}
public void RegisterDynamicCommand(JsValue command)
{
_scriptPlugin.RegisterDynamicCommand(command.ToObject());
}
private object RequestInternal(ScriptPluginWebRequest request)
{
var entered = false;

View File

@ -47,7 +47,6 @@ public class ScriptPluginV2 : IPluginV2
private readonly List<string> _registeredCommandNames = new();
private readonly List<string> _registeredInteractions = new();
private readonly Dictionary<MethodInfo, List<object>> _registeredEvents = new();
private IManager _manager;
private bool _firstInitialization = true;
private record ScriptPluginDetails(string Name, string Author, string Version,
@ -113,15 +112,8 @@ 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
{
@ -261,12 +253,8 @@ public class ScriptPluginV2 : IPluginV2
command.Permission, command.TargetRequired,
command.Arguments, Execute, command.SupportedGames);
manager.RemoveCommandByName(scriptCommand.Name);
manager.AddAdditionalCommand(scriptCommand);
if (!_registeredCommandNames.Contains(scriptCommand.Name))
{
_registeredCommandNames.Add(scriptCommand.Name);
}
_registeredCommandNames.Add(scriptCommand.Name);
}
private void ResetEngineState()
@ -492,33 +480,6 @@ public class ScriptPluginV2 : IPluginV2
}
private static ScriptPluginDetails AsScriptPluginInstance(dynamic source)
{
var commandDetails = ParseScriptCommandDetails(source);
var interactionDetails = Array.Empty<ScriptPluginInteractionDetails>();
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<ScriptPluginCommandDetails>();
if (HasProperty(source, "commands") && source.commands is dynamic[])
@ -552,7 +513,7 @@ public class ScriptPluginV2 : IPluginV2
(bool)command.targetRequired;
var supportedGames =
HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable<object>
? ((IEnumerable<object>)command.supportedGames).Where(game => !string.IsNullOrEmpty(game?.ToString()))
? ((IEnumerable<object>)command.supportedGames).Where(game => game?.ToString() is not null)
.Select(game =>
Enum.Parse<Reference.Game>(game.ToString()!))
: Array.Empty<Reference.Game>();
@ -562,10 +523,31 @@ public class ScriptPluginV2 : IPluginV2
return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired,
commandArgs, supportedGames, execute);
}).ToArray();
}
return commandDetails;
var interactionDetails = Array.Empty<ScriptPluginInteractionDetails>();
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 bool HasProperty(dynamic source, string name)

View File

@ -8,11 +8,9 @@ using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions;
using WebfrontCore.QueryHelpers.Models;
using EFClient = Data.Models.Client.EFClient;
@ -20,7 +18,6 @@ namespace IW4MAdmin.Application.QueryHelpers;
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
{
public ApplicationConfiguration _appConfig { get; }
private readonly IDatabaseContextFactory _contextFactory;
private readonly IGeoLocationService _geoLocationService;
@ -30,10 +27,8 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
public EFAlias Alias { get; set; }
}
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService,
ApplicationConfiguration appConfig)
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService)
{
_appConfig = appConfig;
_contextFactory = contextFactory;
_geoLocationService = geoLocationService;
}
@ -80,9 +75,7 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
if (!string.IsNullOrWhiteSpace(query.ClientIp))
{
clientAliases = SearchByIp(query, clientAliases,
_appConfig.HasPermission(query.RequesterPermission, WebfrontEntity.ClientIPAddress,
WebfrontPermission.Read));
clientAliases = SearchByIp(query, clientAliases);
}
var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection });
@ -210,7 +203,7 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
}
private static IQueryable<ClientAlias> SearchByIp(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases, bool canSearchIP)
IQueryable<ClientAlias> clientAliases)
{
var ipString = query.ClientIp.Trim();
var ipAddress = ipString.ConvertToIP();
@ -220,7 +213,7 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
clientAliases = clientAliases.Where(clientAlias =>
clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress);
}
else if(canSearchIP)
else
{
clientAliases = clientAliases.Where(clientAlias =>
EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%"));

View File

@ -194,14 +194,10 @@ namespace IW4MAdmin.Application.RConParsers
foreach (var line in response)
{
var regex = Regex.Match(line, parserRegex.Pattern);
if (!regex.Success || !parserRegex.GroupMapping.ContainsKey(groupType))
if (regex.Success && parserRegex.GroupMapping.ContainsKey(groupType))
{
continue;
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
}
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
break;
}
if (value == null)
@ -308,7 +304,7 @@ namespace IW4MAdmin.Application.RConParsers
{
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() || (ip == null && ping is 999 or 0) ?
networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ?
name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
}

View File

@ -9,6 +9,7 @@ using Data.Models.Client.Stats;
using Data.Models.Client.Stats.Reference;
using Data.Models.Misc;
using Data.Models.Server;
using Data.Models.Zombie;
namespace Data.Context
{
@ -47,6 +48,16 @@ namespace Data.Context
public DbSet<EFServerSnapshot> ServerSnapshots { get;set; }
public DbSet<EFClientConnectionHistory> ConnectionHistory { get; set; }
#endregion
#region Zombie
public DbSet<ZombieMatch> ZombieMatches { get; set; }
public DbSet<ZombieMatchClientStat> ZombieMatchClientStats { get; set; }
public DbSet<ZombieRoundClientStat> ZombieRoundClientStats { get; set; }
public DbSet<ZombieAggregateClientStat> ZombieClientStatAggregates { get; set; }
public DbSet<ZombieClientStatRecord> ZombieClientStatRecords { get; set; }
#endregion
private void SetAuditColumns()
@ -62,10 +73,6 @@ namespace Data.Context
}
}
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
protected DatabaseContext(DbContextOptions options) : base(options)
{
}
@ -153,8 +160,6 @@ namespace Data.Context
modelBuilder.Entity<EFClientConnectionHistory>(ent => ent.HasIndex(history => history.CreatedDateTime));
modelBuilder.Entity<EFServerSnapshot>(ent => ent.HasIndex(snapshot => snapshot.CapturedAt));
// force full name for database conversion
modelBuilder.Entity<EFClient>().ToTable("EFClients");
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");
@ -163,6 +168,13 @@ namespace Data.Context
modelBuilder.Entity<EFPenaltyIdentifier>().ToTable("EFPenaltyIdentifiers");
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory));
modelBuilder.Entity(typeof(ZombieMatch)).ToTable($"EF{nameof(ZombieMatch)}");
modelBuilder.Entity(typeof(ZombieMatchClientStat)).ToTable($"EF{nameof(ZombieMatchClientStat)}");
modelBuilder.Entity(typeof(ZombieRoundClientStat)).ToTable($"EF{nameof(ZombieRoundClientStat)}");
modelBuilder.Entity(typeof(ZombieAggregateClientStat)).ToTable($"EF{nameof(ZombieAggregateClientStat)}");
modelBuilder.Entity(typeof(ZombieClientStat)).ToTable($"EF{nameof(ZombieClientStat)}");
modelBuilder.Entity(typeof(ZombieClientStatRecord)).ToTable($"EF{nameof(ZombieClientStatRecord)}");
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot",
column: "CapturedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot");
}
}
}

View File

@ -814,7 +814,6 @@ namespace Data.Migrations.MySql
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("varchar(255)")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
@ -1111,8 +1110,6 @@ namespace Data.Migrations.MySql
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");

View File

@ -1,52 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "character varying(255)",
maxLength: 255,
nullable: true,
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
stored: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true,
oldComputedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
oldStored: true);
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot",
column: "CapturedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot");
migrationBuilder.AlterColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "text",
nullable: true,
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
stored: true,
oldClrType: typeof(string),
oldType: "character varying(255)",
oldMaxLength: 255,
oldNullable: true,
oldComputedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
oldStored: true);
}
}
}

View File

@ -853,8 +853,7 @@ namespace Data.Migrations.Postgresql
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnType("text")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
b.Property<string>("SearchableName")
@ -1164,8 +1163,6 @@ namespace Data.Migrations.Postgresql
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");

View File

@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Data.Migrations.Sqlite
{
[DbContext(typeof(SqliteDatabaseContext))]
[Migration("20230705132822_AddIndexToEFServerSnapshotCapturedAt")]
partial class AddIndexToEFServerSnapshotCapturedAt
[Migration("20230507181011_AddZombieStatsInitial")]
partial class AddZombieStatsInitial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
@ -440,6 +440,9 @@ namespace Data.Migrations.Sqlite
b.Property<bool>("Newest")
.HasColumnType("INTEGER");
b.Property<string>("PerformanceBucket")
.HasColumnType("TEXT");
b.Property<double?>("PerformanceMetric")
.HasColumnType("REAL");
@ -1074,6 +1077,9 @@ namespace Data.Migrations.Sqlite
b.Property<bool>("IsPasswordProtected")
.HasColumnType("INTEGER");
b.Property<string>("PerformanceBucket")
.HasColumnType("TEXT");
b.Property<int>("Port")
.HasColumnType("INTEGER");
@ -1111,8 +1117,6 @@ namespace Data.Migrations.Sqlite
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");
@ -1165,6 +1169,239 @@ namespace Data.Migrations.Sqlite
b.ToTable("Vector3", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.Property<long>("ZombieClientStatId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int>("DamageDealt")
.HasColumnType("INTEGER");
b.Property<int>("DamageReceived")
.HasColumnType("INTEGER");
b.Property<int>("Deaths")
.HasColumnType("INTEGER");
b.Property<int>("Downs")
.HasColumnType("INTEGER");
b.Property<int>("Headshots")
.HasColumnType("INTEGER");
b.Property<int>("Kills")
.HasColumnType("INTEGER");
b.Property<int?>("MatchId")
.HasColumnType("INTEGER");
b.Property<int>("Melees")
.HasColumnType("INTEGER");
b.Property<int>("PerksConsumed")
.HasColumnType("INTEGER");
b.Property<long>("PointsEarned")
.HasColumnType("INTEGER");
b.Property<long>("PointsSpent")
.HasColumnType("INTEGER");
b.Property<int>("PowerupsGrabbed")
.HasColumnType("INTEGER");
b.Property<int>("Revives")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.HasKey("ZombieClientStatId");
b.HasIndex("ClientId");
b.HasIndex("MatchId");
b.ToTable("EFZombieClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.Property<int>("ZombieClientStatRecordId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long?>("RoundId")
.HasColumnType("INTEGER");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ZombieClientStatRecordId");
b.HasIndex("ClientId");
b.HasIndex("RoundId");
b.ToTable("EFZombieClientStatRecord", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Property<int>("ZombieMatchId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ClientsCompleted")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<int?>("MapId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("MatchEndDate")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("MatchStartDate")
.HasColumnType("TEXT");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.HasKey("ZombieMatchId");
b.HasIndex("EFClientClientId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFZombieMatch", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<double>("AlivePercentage")
.HasColumnType("REAL");
b.Property<double>("AverageDowns")
.HasColumnType("REAL");
b.Property<double>("AverageKillsPerDown")
.HasColumnType("REAL");
b.Property<double>("AverageMelees")
.HasColumnType("REAL");
b.Property<double>("AveragePoints")
.HasColumnType("REAL");
b.Property<double>("AverageRevives")
.HasColumnType("REAL");
b.Property<double>("AverageRoundReached")
.HasColumnType("REAL");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<double>("HeadshotPercentage")
.HasColumnType("REAL");
b.Property<int>("HighestRound")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesCompleted")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesPlayed")
.HasColumnType("INTEGER");
b.Property<int>("TotalRoundsPlayed")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieAggregateClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieMatchClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<TimeSpan?>("Duration")
.HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("EndTime")
.HasColumnType("TEXT");
b.Property<int>("Points")
.HasColumnType("INTEGER");
b.Property<int>("RoundNumber")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("StartTime")
.HasColumnType("TEXT");
b.Property<TimeSpan?>("TimeAlive")
.HasColumnType("TEXT");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieRoundClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b =>
{
b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot")
@ -1606,6 +1843,96 @@ namespace Data.Migrations.Sqlite
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany("ZombieClientStats")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany("ClientStats")
.HasForeignKey("MatchId");
b.Navigation("Client");
b.Navigation("Match");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId");
b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round")
.WithMany()
.HasForeignKey("RoundId");
b.Navigation("Client");
b.Navigation("Round");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatches")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId");
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.Navigation("Map");
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieAggregateClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatchClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieRoundClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClient", b =>
{
b.Navigation("AdministeredPenalties");
@ -1613,6 +1940,16 @@ namespace Data.Migrations.Sqlite
b.Navigation("Meta");
b.Navigation("ReceivedPenalties");
b.Navigation("ZombieAggregateClientStats");
b.Navigation("ZombieClientStats");
b.Navigation("ZombieMatchClientStats");
b.Navigation("ZombieMatches");
b.Navigation("ZombieRoundClientStats");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>
@ -1636,6 +1973,11 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Navigation("ClientStats");
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,297 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddZombieStatsInitial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PerformanceBucket",
table: "EFServers",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PerformanceBucket",
table: "EFClientRankingHistory",
type: "TEXT",
nullable: true);
migrationBuilder.CreateTable(
name: "EFZombieMatch",
columns: table => new
{
ZombieMatchId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MapId = table.Column<int>(type: "INTEGER", nullable: true),
ServerId = table.Column<long>(type: "INTEGER", nullable: true),
ClientsCompleted = table.Column<int>(type: "INTEGER", nullable: false),
MatchStartDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
MatchEndDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true),
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieMatch", x => x.ZombieMatchId);
table.ForeignKey(
name: "FK_EFZombieMatch_EFClients_EFClientClientId",
column: x => x.EFClientClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey(
name: "FK_EFZombieMatch_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId");
table.ForeignKey(
name: "FK_EFZombieMatch_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId");
});
migrationBuilder.CreateTable(
name: "EFZombieClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MatchId = table.Column<int>(type: "INTEGER", nullable: true),
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
Kills = table.Column<int>(type: "INTEGER", nullable: false),
Deaths = table.Column<int>(type: "INTEGER", nullable: false),
DamageDealt = table.Column<int>(type: "INTEGER", nullable: false),
DamageReceived = table.Column<int>(type: "INTEGER", nullable: false),
Headshots = table.Column<int>(type: "INTEGER", nullable: false),
Melees = table.Column<int>(type: "INTEGER", nullable: false),
Downs = table.Column<int>(type: "INTEGER", nullable: false),
Revives = table.Column<int>(type: "INTEGER", nullable: false),
PointsEarned = table.Column<long>(type: "INTEGER", nullable: false),
PointsSpent = table.Column<long>(type: "INTEGER", nullable: false),
PerksConsumed = table.Column<int>(type: "INTEGER", nullable: false),
PowerupsGrabbed = table.Column<int>(type: "INTEGER", nullable: false),
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieClientStat_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFZombieClientStat_EFZombieMatch_MatchId",
column: x => x.MatchId,
principalTable: "EFZombieMatch",
principalColumn: "ZombieMatchId");
});
migrationBuilder.CreateTable(
name: "EFZombieAggregateClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
AverageKillsPerDown = table.Column<double>(type: "REAL", nullable: false),
AverageDowns = table.Column<double>(type: "REAL", nullable: false),
AverageRevives = table.Column<double>(type: "REAL", nullable: false),
HeadshotPercentage = table.Column<double>(type: "REAL", nullable: false),
AlivePercentage = table.Column<double>(type: "REAL", nullable: false),
AverageMelees = table.Column<double>(type: "REAL", nullable: false),
AverageRoundReached = table.Column<double>(type: "REAL", nullable: false),
AveragePoints = table.Column<double>(type: "REAL", nullable: false),
HighestRound = table.Column<int>(type: "INTEGER", nullable: false),
TotalRoundsPlayed = table.Column<int>(type: "INTEGER", nullable: false),
TotalMatchesPlayed = table.Column<int>(type: "INTEGER", nullable: false),
TotalMatchesCompleted = table.Column<int>(type: "INTEGER", nullable: false),
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieAggregateClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieAggregateClientStat_EFClients_EFClientClientId",
column: x => x.EFClientClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey(
name: "FK_EFZombieAggregateClientStat_EFZombieClientStat_ZombieClientStatId",
column: x => x.ZombieClientStatId,
principalTable: "EFZombieClientStat",
principalColumn: "ZombieClientStatId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFZombieMatchClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieMatchClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieMatchClientStat_EFClients_EFClientClientId",
column: x => x.EFClientClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey(
name: "FK_EFZombieMatchClientStat_EFZombieClientStat_ZombieClientStatId",
column: x => x.ZombieClientStatId,
principalTable: "EFZombieClientStat",
principalColumn: "ZombieClientStatId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFZombieRoundClientStat",
columns: table => new
{
ZombieClientStatId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
StartTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
EndTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Duration = table.Column<TimeSpan>(type: "TEXT", nullable: true),
TimeAlive = table.Column<TimeSpan>(type: "TEXT", nullable: true),
RoundNumber = table.Column<int>(type: "INTEGER", nullable: false),
Points = table.Column<int>(type: "INTEGER", nullable: false),
EFClientClientId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieRoundClientStat", x => x.ZombieClientStatId);
table.ForeignKey(
name: "FK_EFZombieRoundClientStat_EFClients_EFClientClientId",
column: x => x.EFClientClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey(
name: "FK_EFZombieRoundClientStat_EFZombieClientStat_ZombieClientStatId",
column: x => x.ZombieClientStatId,
principalTable: "EFZombieClientStat",
principalColumn: "ZombieClientStatId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFZombieClientStatRecord",
columns: table => new
{
ZombieClientStatRecordId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: true),
RoundId = table.Column<long>(type: "INTEGER", nullable: true),
CreatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedDateTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EFZombieClientStatRecord", x => x.ZombieClientStatRecordId);
table.ForeignKey(
name: "FK_EFZombieClientStatRecord_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId");
table.ForeignKey(
name: "FK_EFZombieClientStatRecord_EFZombieRoundClientStat_RoundId",
column: x => x.RoundId,
principalTable: "EFZombieRoundClientStat",
principalColumn: "ZombieClientStatId");
});
migrationBuilder.CreateIndex(
name: "IX_EFZombieAggregateClientStat_EFClientClientId",
table: "EFZombieAggregateClientStat",
column: "EFClientClientId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieClientStat_ClientId",
table: "EFZombieClientStat",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieClientStat_MatchId",
table: "EFZombieClientStat",
column: "MatchId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieClientStatRecord_ClientId",
table: "EFZombieClientStatRecord",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieClientStatRecord_RoundId",
table: "EFZombieClientStatRecord",
column: "RoundId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieMatch_EFClientClientId",
table: "EFZombieMatch",
column: "EFClientClientId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieMatch_MapId",
table: "EFZombieMatch",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieMatch_ServerId",
table: "EFZombieMatch",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieMatchClientStat_EFClientClientId",
table: "EFZombieMatchClientStat",
column: "EFClientClientId");
migrationBuilder.CreateIndex(
name: "IX_EFZombieRoundClientStat_EFClientClientId",
table: "EFZombieRoundClientStat",
column: "EFClientClientId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFZombieAggregateClientStat");
migrationBuilder.DropTable(
name: "EFZombieClientStatRecord");
migrationBuilder.DropTable(
name: "EFZombieMatchClientStat");
migrationBuilder.DropTable(
name: "EFZombieRoundClientStat");
migrationBuilder.DropTable(
name: "EFZombieClientStat");
migrationBuilder.DropTable(
name: "EFZombieMatch");
migrationBuilder.DropColumn(
name: "PerformanceBucket",
table: "EFServers");
migrationBuilder.DropColumn(
name: "PerformanceBucket",
table: "EFClientRankingHistory");
}
}
}

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot",
column: "CapturedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot");
}
}
}

View File

@ -438,6 +438,9 @@ namespace Data.Migrations.Sqlite
b.Property<bool>("Newest")
.HasColumnType("INTEGER");
b.Property<string>("PerformanceBucket")
.HasColumnType("TEXT");
b.Property<double?>("PerformanceMetric")
.HasColumnType("REAL");
@ -1072,6 +1075,9 @@ namespace Data.Migrations.Sqlite
b.Property<bool>("IsPasswordProtected")
.HasColumnType("INTEGER");
b.Property<string>("PerformanceBucket")
.HasColumnType("TEXT");
b.Property<int>("Port")
.HasColumnType("INTEGER");
@ -1109,8 +1115,6 @@ namespace Data.Migrations.Sqlite
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");
@ -1163,6 +1167,239 @@ namespace Data.Migrations.Sqlite
b.ToTable("Vector3", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.Property<long>("ZombieClientStatId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int>("DamageDealt")
.HasColumnType("INTEGER");
b.Property<int>("DamageReceived")
.HasColumnType("INTEGER");
b.Property<int>("Deaths")
.HasColumnType("INTEGER");
b.Property<int>("Downs")
.HasColumnType("INTEGER");
b.Property<int>("Headshots")
.HasColumnType("INTEGER");
b.Property<int>("Kills")
.HasColumnType("INTEGER");
b.Property<int?>("MatchId")
.HasColumnType("INTEGER");
b.Property<int>("Melees")
.HasColumnType("INTEGER");
b.Property<int>("PerksConsumed")
.HasColumnType("INTEGER");
b.Property<long>("PointsEarned")
.HasColumnType("INTEGER");
b.Property<long>("PointsSpent")
.HasColumnType("INTEGER");
b.Property<int>("PowerupsGrabbed")
.HasColumnType("INTEGER");
b.Property<int>("Revives")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.HasKey("ZombieClientStatId");
b.HasIndex("ClientId");
b.HasIndex("MatchId");
b.ToTable("EFZombieClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.Property<int>("ZombieClientStatRecordId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<long?>("RoundId")
.HasColumnType("INTEGER");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("ZombieClientStatRecordId");
b.HasIndex("ClientId");
b.HasIndex("RoundId");
b.ToTable("EFZombieClientStatRecord", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Property<int>("ZombieMatchId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ClientsCompleted")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("CreatedDateTime")
.HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<int?>("MapId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("MatchEndDate")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("MatchStartDate")
.HasColumnType("TEXT");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("UpdatedDateTime")
.HasColumnType("TEXT");
b.HasKey("ZombieMatchId");
b.HasIndex("EFClientClientId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFZombieMatch", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<double>("AlivePercentage")
.HasColumnType("REAL");
b.Property<double>("AverageDowns")
.HasColumnType("REAL");
b.Property<double>("AverageKillsPerDown")
.HasColumnType("REAL");
b.Property<double>("AverageMelees")
.HasColumnType("REAL");
b.Property<double>("AveragePoints")
.HasColumnType("REAL");
b.Property<double>("AverageRevives")
.HasColumnType("REAL");
b.Property<double>("AverageRoundReached")
.HasColumnType("REAL");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<double>("HeadshotPercentage")
.HasColumnType("REAL");
b.Property<int>("HighestRound")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesCompleted")
.HasColumnType("INTEGER");
b.Property<int>("TotalMatchesPlayed")
.HasColumnType("INTEGER");
b.Property<int>("TotalRoundsPlayed")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieAggregateClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieMatchClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasBaseType("Data.Models.Zombie.ZombieClientStat");
b.Property<TimeSpan?>("Duration")
.HasColumnType("TEXT");
b.Property<int?>("EFClientClientId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("EndTime")
.HasColumnType("TEXT");
b.Property<int>("Points")
.HasColumnType("INTEGER");
b.Property<int>("RoundNumber")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("StartTime")
.HasColumnType("TEXT");
b.Property<TimeSpan?>("TimeAlive")
.HasColumnType("TEXT");
b.HasIndex("EFClientClientId");
b.ToTable("EFZombieRoundClientStat", (string)null);
});
modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b =>
{
b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot")
@ -1604,6 +1841,96 @@ namespace Data.Migrations.Sqlite
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany("ZombieClientStats")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Zombie.ZombieMatch", "Match")
.WithMany("ClientStats")
.HasForeignKey("MatchId");
b.Navigation("Client");
b.Navigation("Match");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieClientStatRecord", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId");
b.HasOne("Data.Models.Zombie.ZombieRoundClientStat", "Round")
.WithMany()
.HasForeignKey("RoundId");
b.Navigation("Client");
b.Navigation("Round");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatches")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId");
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.Navigation("Map");
b.Navigation("Server");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieAggregateClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieAggregateClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieAggregateClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatchClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieMatchClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieMatchClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Zombie.ZombieRoundClientStat", b =>
{
b.HasOne("Data.Models.Client.EFClient", null)
.WithMany("ZombieRoundClientStats")
.HasForeignKey("EFClientClientId");
b.HasOne("Data.Models.Zombie.ZombieClientStat", null)
.WithOne()
.HasForeignKey("Data.Models.Zombie.ZombieRoundClientStat", "ZombieClientStatId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClient", b =>
{
b.Navigation("AdministeredPenalties");
@ -1611,6 +1938,16 @@ namespace Data.Migrations.Sqlite
b.Navigation("Meta");
b.Navigation("ReceivedPenalties");
b.Navigation("ZombieAggregateClientStats");
b.Navigation("ZombieClientStats");
b.Navigation("ZombieMatchClientStats");
b.Navigation("ZombieMatches");
b.Navigation("ZombieRoundClientStats");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b =>
@ -1634,6 +1971,11 @@ namespace Data.Migrations.Sqlite
b.Navigation("ReceivedPenalties");
});
modelBuilder.Entity("Data.Models.Zombie.ZombieMatch", b =>
{
b.Navigation("ClientStats");
});
#pragma warning restore 612, 618
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Zombie;
namespace Data.Models.Client
{
@ -83,5 +84,10 @@ namespace Data.Models.Client
public virtual ICollection<EFMeta> Meta { get; set; }
public virtual ICollection<EFPenalty> ReceivedPenalties { get; set; }
public virtual ICollection<EFPenalty> AdministeredPenalties { get; set; }
public virtual ICollection<ZombieAggregateClientStat> ZombieAggregateClientStats { get; set; }
public virtual ICollection<ZombieClientStat> ZombieClientStats { get; set; }
public virtual ICollection<ZombieMatch> ZombieMatches { get; set; }
public virtual ICollection<ZombieMatchClientStat> ZombieMatchClientStats { get; set; }
public virtual ICollection<ZombieRoundClientStat> ZombieRoundClientStats { get; set; }
}
}

View File

@ -25,5 +25,6 @@ namespace Data.Models.Client.Stats
public int? Ranking { get; set; }
public double? ZScore { get; set; }
public double? PerformanceMetric { get; set; }
public string PerformanceBucket { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Data.Models;
public class DatedRecord
{
public DateTimeOffset CreatedDateTime { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? UpdatedDateTime { get; set; }
}

View File

@ -16,8 +16,7 @@
T7 = 8,
SHG1 = 9,
CSGO = 10,
H1 = 11,
L4D2 = 12,
H1 = 11
}
public enum ConnectionType

View File

@ -15,6 +15,7 @@ namespace Data.Models.Server
public Reference.Game? GameName { get; set; }
public string HostName { get; set; }
public bool IsPasswordProtected { get; set; }
public string PerformanceBucket { get; set; }
public long Id => ServerId;
public string Value => EndPoint;
}

View File

@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Models.Zombie;
public class ZombieAggregateClientStat : ZombieClientStat
{
#region Average
public double AverageKillsPerDown { get; set; }
public double AverageDowns { get; set; }
public double AverageRevives { get; set; }
public double HeadshotPercentage { get; set; }
public double AlivePercentage { get; set; }
public double AverageMelees { get; set; }
public double AverageRoundReached { get; set; }
public double AveragePoints { get; set; }
#endregion
#region Totals
public int HighestRound { get; set; }
public int TotalRoundsPlayed { get; set; }
public int TotalMatchesPlayed { get; set; }
public int TotalMatchesCompleted { get; set; }
#endregion
[NotMapped]
public static readonly string[] RecordsKeys =
{
nameof(AverageKillsPerDown),
nameof(AverageDowns),
nameof(AverageRevives),
nameof(HeadshotPercentage),
nameof(AlivePercentage),
nameof(AverageMelees),
nameof(AverageRoundReached),
nameof(AveragePoints),
nameof(HighestRound),
nameof(TotalRoundsPlayed),
nameof(TotalMatchesPlayed)
};
}

View File

@ -0,0 +1,34 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client;
namespace Data.Models.Zombie;
public abstract class ZombieClientStat : DatedRecord
{
[Key]
public long ZombieClientStatId { get; set; }
public int? MatchId { get; set; }
[ForeignKey(nameof(MatchId))]
public virtual ZombieMatch? Match { get; set; }
public int ClientId { get; set; }
[ForeignKey(nameof(ClientId))]
public virtual EFClient? Client { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public int DamageDealt { get; set; }
public int DamageReceived { get; set; }
public int Headshots { get; set; }
public int Melees { get; set; }
public int Downs { get; set; }
public int Revives { get; set; }
public long PointsEarned { get; set; }
public long PointsSpent { get; set; }
public int PerksConsumed { get; set; }
public int PowerupsGrabbed { get; set; }
}

View File

@ -0,0 +1,29 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client;
namespace Data.Models.Zombie;
public enum RecordType
{
Maximum,
Minimum
}
public class ZombieClientStatRecord : DatedRecord
{
[Key]
public int ZombieClientStatRecordId { get; set; }
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public int? ClientId { get; set; }
[ForeignKey(nameof(ClientId))]
public virtual EFClient? Client { get; set; }
public long? RoundId { get; set; }
[ForeignKey(nameof(RoundId))]
public virtual ZombieRoundClientStat? Round { get; set; }
}

View File

@ -0,0 +1,30 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Data.Models.Client.Stats.Reference;
using Data.Models.Server;
namespace Data.Models.Zombie;
public class ZombieMatch : DatedRecord
{
[Key]
public int ZombieMatchId { get; set; }
public int? MapId { get; set; }
[ForeignKey(nameof(MapId))]
public virtual EFMap? Map { get; set; }
public long? ServerId { get; set; }
[ForeignKey(nameof(ServerId))]
public virtual EFServer? Server { get; set; }
public int ClientsCompleted { get; set; }
public virtual ICollection<ZombieClientStat>? ClientStats { get; set; }
public DateTimeOffset MatchStartDate { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? MatchEndDate { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace Data.Models.Zombie;
public class ZombieMatchClientStat : ZombieClientStat
{
}

View File

@ -0,0 +1,13 @@
using System;
namespace Data.Models.Zombie;
public class ZombieRoundClientStat : ZombieClientStat
{
public DateTimeOffset StartTime { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? EndTime { get; set; }
public TimeSpan? Duration { get; set; }
public TimeSpan? TimeAlive { get; set; }
public int RoundNumber { get; set; }
public int Points { get; set; }
}

View File

@ -6,7 +6,6 @@ trigger:
include:
- release/pre
- master
- develop
pr: none
@ -21,233 +20,227 @@ variables:
buildConfiguration: Stable
isPreRelease: 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: 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
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: 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: 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: ArchiveFiles@2
displayName: 'Generate final zip file'
inputs:
rootFolderOrFile: '$(outputFolder)'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip'
replaceExistingArchive: true
- 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: 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: PowerShell@2
displayName: 'Run publish script 1'
inputs:
filePath: 'DeploymentFiles/PostPublish.ps1'
arguments: '$(outputFolder)'
failOnStderr: true
workingDirectory: '$(Build.Repository.LocalPath)'
- 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
- 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
$params = @{
Uri = 'http://api.raidmax.org:5000/version'
Method = 'POST'
Body = $payload
ContentType = 'application/json'
}
Invoke-RestMethod @params
- task: PublishPipelineArtifact@1
displayName: 'Publish artifact for analysis'
inputs:
targetPath: '$(outputFolder)'
artifact: 'IW4MAdmin.$(buildConfiguration)'
publishLocation: 'pipeline'

View File

@ -0,0 +1,263 @@
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include common_scripts\utility;
init()
{
SetDvarIfUninitialized( "sv_customcallbacks", true );
SetDvarIfUninitialized( "sv_framewaittime", 0.05 );
SetDvarIfUninitialized( "sv_additionalwaittime", 0.1 );
SetDvarIfUninitialized( "sv_maxstoredframes", 12 );
SetDvarIfUninitialized( "sv_printradarupdates", 0 );
SetDvarIfUninitialized( "sv_printradar_updateinterval", 500 );
SetDvarIfUninitialized( "sv_iw4madmin_url", "http://127.0.0.1:1624" );
level thread onPlayerConnect();
if (getDvarInt("sv_printradarupdates") == 1)
{
level thread runRadarUpdates();
}
level waittill( "prematch_over" );
level.callbackPlayerKilled = ::Callback_PlayerKilled;
level.callbackPlayerDamage = ::Callback_PlayerDamage;
level.callbackPlayerDisconnect = ::Callback_PlayerDisconnect;
}
//It's called slightly different in T6
//set_dvar_if_unset(dvar, val, reset)
SetDvarIfUninitialized(dvar, val)
{
set_dvar_if_unset(dvar,val);
}
onPlayerConnect( player )
{
for( ;; )
{
level waittill( "connected", player );
player thread waitForFrameThread();
player thread waitForAttack();
}
}
//Got added to T6 on April 2020
waitForAttack()
{
self endon( "disconnect" );
self notifyOnPlayerCommand( "player_shot", "+attack" );
self.lastAttackTime = 0;
for( ;; )
{
self waittill( "player_shot" );
self.lastAttackTime = getTime();
}
}
runRadarUpdates()
{
interval = getDvarInt( "sv_printradar_updateinterval" );
for ( ;; )
{
for ( i = 0; i <= 17; i++ )
{
player = level.players[i];
if ( isDefined( player ) )
{
payload = player.guid + ";" + player.origin + ";" + player getPlayerAngles() + ";" + player.team + ";" + player.kills + ";" + player.deaths + ";" + player.score + ";" + player GetCurrentWeapon() + ";" + player.health + ";" + isAlive(player) + ";" + player.timePlayed["total"];
logPrint( "LiveRadar;" + payload + "\n" );
}
}
wait( interval / 1000 );
}
}
hitLocationToBone( hitloc )
{
switch( hitloc )
{
case "helmet":
return "j_helmet";
case "head":
return "j_head";
case "neck":
return "j_neck";
case "torso_upper":
return "j_spineupper";
case "torso_lower":
return "j_spinelower";
case "right_arm_upper":
return "j_shoulder_ri";
case "left_arm_upper":
return "j_shoulder_le";
case "right_arm_lower":
return "j_elbow_ri";
case "left_arm_lower":
return "j_elbow_le";
case "right_hand":
return "j_wrist_ri";
case "left_hand":
return "j_wrist_le";
case "right_leg_upper":
return "j_hip_ri";
case "left_leg_upper":
return "j_hip_le";
case "right_leg_lower":
return "j_knee_ri";
case "left_leg_lower":
return "j_knee_le";
case "right_foot":
return "j_ankle_ri";
case "left_foot":
return "j_ankle_le";
default:
return "tag_origin";
}
}
waitForFrameThread()
{
self endon( "disconnect" );
self.currentAnglePosition = 0;
self.anglePositions = [];
for (i = 0; i < getDvarInt( "sv_maxstoredframes" ); i++)
{
self.anglePositions[i] = self getPlayerAngles();
}
for( ;; )
{
self.anglePositions[self.currentAnglePosition] = self getPlayerAngles();
wait( getDvarFloat( "sv_framewaittime" ) );
self.currentAnglePosition = (self.currentAnglePosition + 1) % getDvarInt( "sv_maxstoredframes" );
}
}
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
{
currentIndex = self.currentAnglePosition;
wait( 0.05 * afterFrameCount );
self.angleSnapshot = [];
for( j = 0; j < self.anglePositions.size; j++ )
{
self.angleSnapshot[j] = self.anglePositions[j];
}
anglesStr = "";
collectedFrames = 0;
i = currentIndex - beforeFrameCount;
while (collectedFrames < beforeFrameCount)
{
fixedIndex = i;
if (i < 0)
{
fixedIndex = self.angleSnapshot.size - abs(i);
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
if (i == currentIndex)
{
anglesStr += self.angleSnapshot[i] + ":";
i++;
}
collectedFrames = 0;
while (collectedFrames < afterFrameCount)
{
fixedIndex = i;
if (i > self.angleSnapshot.size - 1)
{
fixedIndex = i % self.angleSnapshot.size;
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
}
vectorScale( vector, scale )
{
return ( vector[0] * scale, vector[1] * scale, vector[2] * scale );
}
Process_Hit( type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon )
{
if (sMeansOfDeath == "MOD_FALLING" || !isPlayer(attacker))
{
return;
}
victim = self;
_attacker = attacker;
if ( !isPlayer( attacker ) && isDefined( attacker.owner ) )
{
_attacker = attacker.owner;
}
else if( !isPlayer( attacker ) && sMeansOfDeath == "MOD_FALLING" )
{
_attacker = victim;
}
location = victim GetTagOrigin( hitLocationToBone( sHitLoc ) );
isKillstreakKill = false;
if(!isPlayer(attacker))
{
isKillstreakKill = true;
}
if(maps/mp/killstreaks/_killstreaks::iskillstreakweapon(sWeapon))
{
isKillstreakKill = true;
}
logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + int(gettime()) + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";0;0";
attacker thread waitForAdditionalAngles( logLine, 2, 2 );
}
Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex )
{
if ( level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team ) )
{
return;
}
if ( self.health - iDamage > 0 )
{
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
}
self [[maps/mp/gametypes/_globallogic_player::callback_playerdamage]]( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex );
}
Callback_PlayerKilled(eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration)
{
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
self [[maps/mp/gametypes/_globallogic_player::callback_playerkilled]]( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
}
Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self [[maps/mp/gametypes/_globallogic_player::callback_playerdisconnect]]();
}

View File

@ -1,4 +1,6 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
Init()
{
@ -8,7 +10,7 @@ Init()
Setup()
{
level endon( "game_ended" );
// setup default vars
level.eventBus = spawnstruct();
level.eventBus.inVar = "sv_iw4madmin_in";
@ -16,54 +18,29 @@ Setup()
level.eventBus.failKey = "fail";
level.eventBus.timeoutKey = "timeout";
level.eventBus.timeout = 30;
level.commonFunctions = spawnstruct();
level.commonFunctions.setDvar = "SetDvarIfUninitialized";
level.commonFunctions.getPlayerFromClientNum = "GetPlayerFromClientNum";
level.commonFunctions.waittillNotifyOrTimeout = "WaittillNotifyOrTimeout";
level.commonFunctions.getInboundData = "GetInboundData";
level.commonFunctions.getOutboundData = "GetOutboundData";
level.commonFunctions.setInboundData = "SetInboundData";
level.commonFunctions.setOutboundData = "SetOutboundData";
level.overrideMethods = [];
level.overrideMethods[level.commonFunctions.setDvar] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getPlayerFromClientNum] = ::_GetPlayerFromClientNum;
level.overrideMethods[level.commonFunctions.getInboundData] = ::_GetInboundData;
level.overrideMethods[level.commonFunctions.getOutboundData] = ::_GetOutboundData;
level.overrideMethods[level.commonFunctions.setInboundData] = ::_SetInboundData;
level.overrideMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData;
level.busMethods = [];
level.busMethods[level.commonFunctions.getInboundData] = ::_GetInboundData;
level.busMethods[level.commonFunctions.getOutboundData] = ::_GetOutboundData;
level.busMethods[level.commonFunctions.setInboundData] = ::_SetInboundData;
level.busMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData;
level.commonFunctions = spawnstruct();
level.commonFunctions.setDvar = "SetDvarIfUninitialized";
level.commonFunctions.isBot = "IsBot";
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";
level.notifyTypes.sharedFunctionsInitialized = "SharedFunctionsInitialized";
level.notifyTypes.integrationBootstrapInitialized = "IntegrationBootstrapInitialized";
level.clientDataKey = "clientData";
level.eventTypes = spawnstruct();
level.eventTypes.eventAvailable = "EventAvailable";
level.eventTypes.localClientEvent = "client_event";
level.eventTypes.clientDataReceived = "ClientDataReceived";
level.eventTypes.clientDataRequested = "ClientDataRequested";
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
level.iw4madminIntegrationDebug = 0;
// map the event type to the handler
level.eventCallbacks = [];
level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived;
@ -73,71 +50,177 @@ Setup()
level.clientCommandCallbacks = [];
level.clientCommandRusAsTarget = [];
level.logger = spawnstruct();
level.overrideMethods = [];
level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
InitializeLogger();
wait ( 0.05 * 2 ); // needed to give script engine time to propagate notifies
wait ( 0.05 ); // needed to give script engine time to propagate notifies
level notify( level.notifyTypes.integrationBootstrapInitialized );
level waittill( level.notifyTypes.gameFunctionsInitialized );
LogDebug( "Integration received notify that game functions are initialized" );
_SetDvarIfUninitialized( level.eventBus.inVar, "" );
_SetDvarIfUninitialized( level.eventBus.outVar, "" );
_SetDvarIfUninitialized( level.commonKeys.enabled, 1 );
_SetDvarIfUninitialized( level.commonKeys.busMode, "rcon" );
_SetDvarIfUninitialized( level.commonKeys.busdir, "" );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
_SetDvarIfUninitialized( "GroupSeparatorChar", "" );
_SetDvarIfUninitialized( "RecordSeparatorChar", "" );
_SetDvarIfUninitialized( "UnitSeparatorChar", "" );
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
// start long running tasks
thread MonitorEvents();
thread MonitorBus();
level thread MonitorClientEvents();
level thread MonitorBus();
level thread OnPlayerConnect();
}
MonitorEvents()
//////////////////////////////////
// Client Methods
//////////////////////////////////
OnPlayerConnect()
{
level endon( level.eventTypes.gameEnd );
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();
player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval();
}
}
OnPlayerSpawned()
{
self endon( "disconnect" );
for ( ;; )
{
self waittill( "spawned_player" );
self PlayerSpawnEvents();
}
}
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 ) );
}
}
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();
}
PlayerTrackingOnInterval()
{
self endon( "disconnect" );
for ( ;; )
{
wait ( 120 );
if ( IsAlive( self ) )
{
self SaveTrackingMetrics();
}
}
}
MonitorClientEvents()
{
level endon( "game_ended" );
for ( ;; )
{
level waittill( level.eventTypes.eventAvailable, event );
level waittill( level.eventTypes.localClientEvent, client );
LogDebug( "Processing Event " + event.type + "-" + event.subtype );
eventHandler = level.eventCallbacks[event.type];
LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
eventHandler = level.eventCallbacks[client.event.type];
if ( IsDefined( eventHandler ) )
{
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 [[eventHandler]]( client.event );
LogDebug( "notify client for " + client.event.type );
client notify( level.eventTypes.localClientEvent, client.event );
}
client.eventData = [];
}
}
@ -145,13 +228,11 @@ MonitorEvents()
// Helper Methods
//////////////////////////////////
NotImplementedFunction( a, b, c, d, e, f )
_IsBot( entity )
{
LogWarning( "Function not implemented" );
if ( IsDefined ( a ) )
{
LogWarning( a );
}
// 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"];
}
_SetDvarIfUninitialized( dvarName, dvarValue )
@ -159,44 +240,9 @@ _SetDvarIfUninitialized( dvarName, dvarValue )
[[level.overrideMethods[level.commonFunctions.setDvar]]]( dvarName, dvarValue );
}
_GetPlayerFromClientNum( clientNum )
NotImplementedFunction( a, b, c, d, e, f )
{
assertEx( clientNum >= 0, "clientNum cannot be negative" );
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 );
LogWarning( "Function not implemented" );
}
// Not every game can output to console or even game log.
@ -217,7 +263,7 @@ _Log( LogLevel, message )
{
for( i = 0; i < level.logger._logger.size; i++ )
{
[[level.logger._logger[i]]]( LogLevel, GetSubStr( message, 0, 1000 ) );
[[level.logger._logger[i]]]( LogLevel, message );
}
}
@ -279,13 +325,13 @@ RegisterLogger( logger )
RequestClientMeta( metaKey )
{
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
level thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
}
RequestClientBasicData()
{
getClientDataEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "None", self, "" );
thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
level thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
}
IncrementClientMeta( metaKey, incrementValue, clientId )
@ -298,22 +344,51 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
if ( IsDefined( self.joining_team ) )
{
team = self.joining_team;
}
else
{
if ( isSpectator || !IsDefined( team ) )
{
team = "spectator";
}
}
guid = self GetXuid();
if ( guid == "0" )
{
guid = self.guid;
}
if ( !IsDefined( guid ) || guid == "0" )
{
guid = "undefined";
}
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
}
SetClientMeta( metaKey, metaValue, clientId, direction )
{
data = [];
data["key"] = metaKey;
data["value"] = metaValue;
data = "key=" + metaKey + "|value=" + metaValue;
clientNumber = -1;
if ( IsDefined ( clientId ) )
{
data["clientId"] = clientId;
data = data + "|clientId=" + clientId;
clientNumber = -1;
}
if ( IsDefined( direction ) )
{
data["direction"] = direction;
data = data + "|direction=" + direction;
}
if ( IsPlayer( self ) )
@ -322,7 +397,40 @@ SetClientMeta( metaKey, metaValue, clientId, direction )
}
setClientMetaEvent = BuildEventRequest( true, level.eventTypes.setClientDataRequested, "Meta", clientNumber, data );
thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
level thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
}
SaveTrackingMetrics()
{
if ( !IsDefined( self.persistentClientId ) )
{
return;
}
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;
LogDebug( "Total Shots Fired increased by " + change );
if ( !IsDefined( change ) )
{
change = 0;
}
if ( change == 0 )
{
return;
}
IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
}
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
@ -331,97 +439,79 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
{
data = "";
}
if ( !IsDefined( eventSubtype ) )
{
eventSubtype = "None";
}
if ( !IsDefined( entOrId ) )
{
entOrId = "-1";
}
if ( IsPlayer( entOrId ) )
{
entOrId = entOrId getEntityNumber();
}
request = "0";
if ( responseExpected )
{
request = "1";
}
data = BuildDataString( data );
groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 );
request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data;
request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data;
return request;
}
MonitorBus()
{
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, "" );
level endon( "game_ended" );
for( ;; )
{
wait ( 0.1 );
// check to see if IW4MAdmin is ready to receive more data
inVal = [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation );
if ( !IsDefined( inVal ) || inVal == "" )
if ( getDvar( level.eventBus.inVar ) == "" )
{
level notify( "bus_ready" );
}
eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]]( level.eventBus.outLocation );
if ( !IsDefined( eventString ) || eventString == "" )
eventString = getDvar( level.eventBus.outVar );
if ( eventString == "" )
{
continue;
}
LogDebug( "-> " + eventString );
groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 );
NotifyEvent( strtok( eventString, groupSeparator ) );
[[level.busMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" );
NotifyClientEvent( strtok( eventString, ";" ) );
SetDvar( level.eventBus.outVar, "" );
}
}
QueueEvent( request, eventType, notifyEntity )
{
level endon( level.eventTypes.gameEnd );
level endon( "game_ended" );
start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds
timedOut = "";
while ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" && ( GetTime() - start ) < maxWait )
while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait )
{
level [[level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout]]]( "bus_ready", 1 );
level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 );
if ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" )
if ( GetDvar( level.eventBus.inVar ) != "" )
{
LogDebug( "A request is already in progress..." );
timedOut = "set";
continue;
}
timedOut = "unset";
}
if ( timedOut == "set" )
if ( timedOut == "set")
{
LogDebug( "Timed out waiting for response..." );
@ -430,14 +520,14 @@ QueueEvent( request, eventType, notifyEntity )
notifyEntity NotifyClientEventTimeout( eventType );
}
[[level.busMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" );
SetDvar( level.eventBus.inVar, "" );
return;
}
LogDebug( "<- " + request );
[[level.busMethods[level.commonFunctions.setInboundData]]]( level.eventBus.inLocation, request );
LogDebug("<- " + request );
SetDvar( level.eventBus.inVar, request );
}
ParseDataString( data )
@ -447,43 +537,23 @@ ParseDataString( data )
LogDebug( "No data to parse" );
return [];
}
dataParts = strtok( data, GetSubStr( GetDvar( "RecordSeparatorChar" ), 0, 1 ) );
dataParts = strtok( data, "|" );
dict = [];
for ( i = 0; i < dataParts.size; i++ )
{
part = dataParts[i];
splitPart = strtok( part, GetSubStr( GetDvar( "UnitSeparatorChar" ), 0, 1 ) );
splitPart = strtok( part, "=" );
key = splitPart[0];
value = splitPart[1];
dict[key] = value;
dict[i] = key;
}
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
@ -493,18 +563,23 @@ NotifyClientEventTimeout( eventType )
}
}
NotifyEvent( eventInfo )
NotifyClientEvent( eventInfo )
{
origin = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[3] ) );
target = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[4] ) );
origin = getPlayerFromClientNum( int( eventInfo[3] ) );
target = getPlayerFromClientNum( int( eventInfo[4] ) );
event = spawnstruct();
event.type = eventInfo[1];
event.subtype = eventInfo[2];
event.data = ParseDataString( eventInfo[5] );
event.data = 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] ) );
@ -514,15 +589,41 @@ NotifyEvent( eventInfo )
LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) );
}
client = event.origin;
if ( !IsDefined( client ) )
if ( IsDefined( target ) )
{
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 );
GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
for ( i = 0; i < level.players.size; i++ )
{
if ( level.players[i] getEntityNumber() == clientNum )
{
return level.players[i];
}
}
return undefined;
}
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
@ -531,7 +632,7 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
{
return;
}
level.clientCommandCallbacks[commandName] = callback;
level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean
}
@ -542,7 +643,7 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
OnClientDataReceived( event )
{
assertEx( isDefined( self ), "player entity is not defined");
event.data = ParseDataString( event.data );
clientData = self.pers[level.clientDataKey];
if ( event.subtype == "Fail" )
@ -558,15 +659,15 @@ OnClientDataReceived( event )
{
clientData.meta = [];
}
metaKey = event.data[0];
clientData.meta[metaKey] = event.data[metaKey];
LogDebug( "Meta Key=" + CoerceUndefined( metaKey ) + ", Meta Value=" + CoerceUndefined( event.data[metaKey] ) );
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
return;
}
clientData.permissionLevel = event.data["level"];
clientData.clientId = event.data["clientId"];
clientData.lastConnection = event.data["lastConnection"];
@ -574,13 +675,15 @@ OnClientDataReceived( event )
clientData.performance = event.data["performance"];
clientData.state = "complete";
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
}
OnExecuteCommand( event )
{
data = event.data;
data = ParseDataString( event.data );
response = "";
command = level.clientCommandCallbacks[event.subtype];
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
executionContextEntity = event.origin;
@ -589,23 +692,16 @@ OnExecuteCommand( event )
{
executionContextEntity = event.target;
}
if ( IsDefined( command ) )
{
if ( IsDefined( executionContextEntity ) )
{
response = executionContextEntity thread [[command]]( event, data );
}
else
{
thread [[command]]( event );
}
response = executionContextEntity [[command]]( event, data );
}
else
{
LogDebug( "Unknown Client command->" + event.subtype );
}
// send back the response to the origin, but only if they're not the target
if ( IsDefined( response ) && response != "" && IsPlayer( event.origin ) && event.origin != event.target )
{
@ -615,15 +711,6 @@ OnExecuteCommand( event )
OnSetClientDataCompleted( event )
{
LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( event.data["status"] ) );
}
CoerceUndefined( object )
{
if ( !IsDefined( object ) )
{
return "undefined";
}
return object;
// IW4MAdmin let us know it persisted (success or fail)
LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
}

View File

@ -2,23 +2,24 @@
Init()
{
level.eventBus.gamename = "IW4";
thread Setup();
}
Setup()
{
level endon( "game_ended" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "IW4";
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
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["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::_SetDvarIfUninitialized;
level.overrideMethods[level.commonFunctions.isBot] = ::IsTestClient;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
level.overrideMethods[level.commonFunctions.changeTeam] = ::ChangeTeam;
level.overrideMethods[level.commonFunctions.getTeamCounts] = ::CountPlayers;
level.overrideMethods[level.commonFunctions.getMaxClients] = ::GetMaxClients;
@ -27,25 +28,17 @@ 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();
level notify( level.notifyTypes.gameFunctionsInitialized );
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.busdir, GetDvar( "fs_homepath" ) + "userraw/" + "scriptdata" );
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
thread OnPlayerConnect();
level thread OnPlayerConnect();
}
OnPlayerConnect()
@ -56,12 +49,12 @@ OnPlayerConnect()
{
level waittill( "connected", player );
if ( player IsTestClient() )
if ( player call [[ level.overrideMethods[ level.commonFunctions.isBot ] ]]() )
{
// we don't want to track bots
continue;
continue;
}
player thread SetPersistentData();
player thread WaitForClientEvents();
}
@ -92,7 +85,7 @@ WaitForClientEvents()
for ( ;; )
{
self waittill( level.eventTypes.eventAvailable, event );
self waittill( level.eventTypes.localClientEvent, event );
scripts\_integration_base::LogDebug( "Received client event " + event.type );
@ -104,26 +97,6 @@ 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;
@ -211,7 +184,12 @@ GetTotalShotsFired()
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
WaitillNotifyOrTimeoutWrapper( _notify, timeout )
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
@ -221,21 +199,6 @@ Log2Console( logLevel, message )
PrintConsole( "[" + logLevel + "] " + message + "\n" );
}
SetDvarIfUninitializedWrapper( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
GetXuidWrapper()
{
return self GetXUID();
}
IsBotWrapper( client )
{
return client IsTestClient();
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
@ -549,7 +512,11 @@ HideImpl()
AlertImpl( event, data )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
if ( level.eventBus.gamename == "IW4" )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}

View File

@ -1,46 +1,92 @@
#include common_scripts\utility;
#inline scripts\_integration_utility;
Init()
{
level.eventBus.gamename = "IW5";
thread Setup();
}
Setup()
{
level endon( "game_ended" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "IW5";
scripts\_integration_base::RegisterLogger( ::Log2Console );
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;
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\mp\_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;
RegisterClientCommands();
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()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_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()
@ -48,12 +94,12 @@ GetTotalShotsFired()
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
SetDvarIfUninitializedWrapper( dvar, value )
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
WaitillNotifyOrTimeoutWrapper( _notify, timeout )
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
@ -63,19 +109,135 @@ Log2Console( logLevel, message )
Print( "[" + logLevel + "] " + message + "\n" );
}
IsBotWrapper( client )
//////////////////////////////////
// GUID helpers
/////////////////////////////////
SetPersistentData()
{
return client IsTestClient();
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\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
GetXuidWrapper()
SplitGuid()
{
return self GetXUID();
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;
}
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
Pow( num, exponent )
{
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
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;
}
}
//////////////////////////////////
@ -84,36 +246,45 @@ WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
GiveWeaponImpl( event, data )
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
@ -123,7 +294,10 @@ TeamSwitchImpl()
LockControlsImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
@ -139,11 +313,11 @@ LockControlsImpl()
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
@ -160,13 +334,11 @@ LockControlsImpl()
NoClipImpl()
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
@ -176,29 +348,29 @@ NoClipImpl()
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self God();
self Noclip();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self God();
self Noclip();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
@ -207,13 +379,12 @@ NoClipImpl()
HideImpl()
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
@ -223,33 +394,36 @@ HideImpl()
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self God();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self God();
self Show();
SetDvar( "sv_cheats", 0 );
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
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 );
}
return "Sent alert to " + self.name;
}
@ -267,8 +441,6 @@ GotoImpl( event, data )
GotoCoordImpl( data )
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
@ -282,8 +454,6 @@ GotoCoordImpl( data )
GotoPlayerImpl( target )
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
@ -296,7 +466,10 @@ GotoPlayerImpl( target )
PlayerToMeImpl( event )
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
@ -304,7 +477,10 @@ PlayerToMeImpl( event )
KillImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
@ -314,15 +490,13 @@ KillImpl()
SetSpectatorImpl()
{
_VERIFY_PLAYER_ENT( self );
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -1,3 +1,4 @@
Init()
{
thread Setup();
@ -5,11 +6,8 @@ Init()
Setup()
{
wait ( 0.05 );
level endon( "game_ended" );
level waittill( level.notifyTypes.integrationBootstrapInitialized );
level.commonFunctions.changeTeam = "ChangeTeam";
level.commonFunctions.getTeamCounts = "GetTeamCounts";
level.commonFunctions.getMaxClients = "GetMaxClients";
@ -17,10 +15,7 @@ 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;
@ -30,52 +25,31 @@ 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[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 )
{
return;
}
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.autoBalance, 0 );
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 )
{
return;
}
thread OnPlayerConnect();
}
_IsBot( player )
{
return [[level.overrideMethods[level.commonFunctions.isBot]]]( player );
level thread OnPlayerConnect();
}
OnPlayerConnect()
@ -85,28 +59,7 @@ OnPlayerConnect()
for ( ;; )
{
level waittill( level.eventTypes.connect, 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( level.commonKeys.autoBalance ) != 1 || !IsDefined( [[level.overrideMethods[level.commonFunctions.getTeamBased]]]() ) )
{
continue;
}
if ( ![[level.overrideMethods[level.commonFunctions.getTeamBased]]]() )
{
continue;
@ -115,341 +68,13 @@ OnPlayerConnect()
teamToJoin = player GetTeamToJoin();
player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin );
player thread OnPlayerFirstSpawn();
player thread OnPlayerDisconnect();
player thread OnClientFirstSpawn();
player thread OnClientJoinedTeam();
player thread 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()
OnClientDisconnect()
{
level endon( level.eventTypes.gameEnd );
self endon( "disconnect_logic_end" );
@ -464,7 +89,7 @@ OnPlayerDisconnect()
}
}
OnPlayerJoinedTeam()
OnClientJoinedTeam()
{
self endon( level.eventTypes.disconnect );
@ -472,14 +97,6 @@ OnPlayerJoinedTeam()
{
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;
@ -492,7 +109,7 @@ OnPlayerJoinedTeam()
if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 )
{
OnTeamSizeChanged();
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
continue;
}
@ -507,34 +124,12 @@ OnPlayerJoinedTeam()
}
}
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()
OnClientFirstSpawn()
{
self endon( level.eventTypes.disconnect );
timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.spawned );
if ( timeoutResult != level.eventBus.timeoutKey )
if ( timeoutResult != "timeout" )
{
return;
}
@ -729,7 +324,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;
}
@ -854,36 +449,3 @@ GetClientPerformanceOrDefault()
return performance;
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
if ( IsDefined( self.joining_team ) )
{
team = self.joining_team;
}
else
{
if ( isSpectator || !IsDefined( team ) )
{
team = "spectator";
}
}
guid = self [[level.overrideMethods[level.commonFunctions.getXuid]]]();
if ( guid == "0" )
{
guid = self.guid;
}
if ( !IsDefined( guid ) || guid == "0" )
{
guid = "undefined";
}
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
}
// #end region

View File

@ -2,43 +2,90 @@
Init()
{
level.eventBus.gamename = "T5";
thread Setup();
}
Setup()
{
level endon( "game_ended" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "T5";
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
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["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
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\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
//player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_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()
@ -46,12 +93,12 @@ GetTotalShotsFired()
return maps\mp\gametypes\_persistence::statGet( "total_shots" );
}
SetDvarIfUninitializedWrapper( dvar, value )
_SetDvarIfUninitialized(dvar, value)
{
maps\mp\_utility::set_dvar_if_unset( dvar, value );
maps\mp\_utility::set_dvar_if_unset(dvar, value);
}
WaitillNotifyOrTimeoutWrapper( msg, timer )
_waittill_notify_or_timeout( msg, timer )
{
self endon( msg );
wait( timer );
@ -64,6 +111,7 @@ Log2Console( logLevel, message )
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
@ -81,16 +129,137 @@ God()
}
}
IsBotWrapper( client )
//////////////////////////////////
// GUID helpers
/////////////////////////////////
/*SetPersistentData()
{
return client maps\mp\_utility::is_bot();
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\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
GetXuidWrapper()
SplitGuid()
{
return self GetGuid();
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
/////////////////////////////////
@ -226,7 +395,7 @@ NoClipImpl( event, data )
self IPrintLnBold( "NoClip enabled" );*/
scripts\_integration_base::LogWarning( "NoClip is not supported on T5!" );
scripts\mp\_integration_base::LogWarning( "NoClip is not supported on T5!" );
}

View File

@ -1,384 +0,0 @@
#include common_scripts\utility;
Init()
{
thread Setup();
}
Setup()
{
level endon( "end_game" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "T5";
level.eventTypes.gameEnd = "end_game";
scripts\_integration_base::RegisterLogger( ::Log2Console );
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();
level notify( level.notifyTypes.gameFunctionsInitialized );
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
GetTotalShotsFired()
{
return 0; //ZM has no shot tracking. TODO: add tracking function for event weapon_fired
}
SetDvarIfUninitializedWrapper( dvar, value )
{
if ( GetDvar( dvar ) == "" )
{
SetDvar( dvar, value );
return value;
}
return GetDvar( dvar );
}
WaitillNotifyOrTimeoutWrapper( msg, timer )
{
self endon( msg );
wait( timer );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
}
if (!self.godmode )
{
self enableInvulnerability();
self.godmode = true;
}
else
{
self.godmode = false;
self disableInvulnerability();
}
}
IsBotWrapper( client )
{
return ( IsDefined ( client.pers["isBot"] ) && client.pers["isBot"] != 0 );
}
GetXuidWrapper()
{
return self GetXUID();
}
_GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
players = GetPlayers( "all" );
for ( i = 0; i < players.size; i++ )
{
scripts\_integration_base::LogDebug( i+"/"+players.size+ "=" + players[i].name );
if ( players[i] getEntityNumber() == clientNum )
{
return players[i];
}
}
return undefined;
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl( event, data )
{
/*if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );*/
scripts\_integration_base::LogWarning( "NoClip is not supported on T5!" );
}
HideImpl( event, data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
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"] );
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl( event, data )
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -1,395 +0,0 @@
#include common_scripts\utility;
#include maps\mp\_utility;
Init()
{
thread Setup();
}
Setup()
{
level endon( "game_ended" );
level endon( "end_game" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "T6";
if ( sessionmodeiszombiesgame() )
{
level.eventTypes.gameEnd = "end_game";
}
scripts\_integration_base::RegisterLogger( ::Log2Console );
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();
level notify( level.notifyTypes.gameFunctionsInitialized );
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
GetTotalShotsFired()
{
return self.pers["total_shots"];
}
SetDvarIfUninitializedWrapper( dvar, value )
{
maps\mp\_utility::set_dvar_if_unset( dvar, value );
}
WaitillNotifyOrTimeoutWrapper( msg, timer )
{
self endon( msg );
wait( timer );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
}
if (!self.godmode )
{
self enableInvulnerability();
self.godmode = true;
}
else
{
self.godmode = false;
self disableInvulnerability();
}
}
IsBotWrapper( client )
{
return client maps\mp\_utility::is_bot();
}
GetXuidWrapper()
{
return self GetXUID();
}
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
{
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( isDefined( level.player_too_many_weapons_monitor ) && level.player_too_many_weapons_monitor )
{
level.player_too_many_weapons_monitor = false;
self notify( "stop_player_too_many_weapons_monitor" );
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl( event, data )
{
/*if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );*/
scripts\_integration_base::LogWarning( "NoClip is not supported on T6!" );
}
HideImpl( event, data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
self thread oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 );
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl( event, data )
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}
//////////////////////////////////
// T6 specific functions
/////////////////////////////////
/*
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 )
{
/*if ( level.wagermatch && !level.teambased )
{
return;
}*/
notifydata = spawnstruct();
notifydata.titletext = titletext;
notifydata.notifytext = notifytext;
notifydata.iconname = iconname;
notifydata.sound = sound;
notifydata.duration = duration;
self.startmessagenotifyqueue[ self.startmessagenotifyqueue.size ] = notifydata;
self notify( "received award" );
}

View File

@ -1,73 +0,0 @@
/*********************************************************************************
* DISCLAIMER: *
* *
* This script is optional and not required for *
* standard functionality. To use this script, a third-party *
* plugin named "t6-gsc-utils" must be installed on the *
* game server in the "*\storage\t6\plugins" folder *
* *
* The "t6-gsc-utils" plugin can be obtained from the GitHub *
* repository at: *
* https://github.com/fedddddd/t6-gsc-utils *
* *
* Please make sure to install the plugin before running this *
* script. *
*********************************************************************************/
/*********************************************************************************
* FUNCTIONALITY: *
* *
* This script extends the game interface to support the "file" *
* bus mode for Plutonium T6, which allows the game server and IW4M-Admin *
* to communicate via files, rather than over rcon using *
* dvars. *
* *
* By enabling the "file" bus mode, IW4M-Admin can send *
* commands and receive responses from the game server by *
* reading and writing to specific files. This provides a *
* flexible and efficient communication channel. *
* *
* Make sure to configure the server to use the "file" bus *
* mode and set the appropriate file path to *
* establish the communication between IW4M-Admin and the *
* game server. *
* *
* The wiki page for the setup of the game interface, and the bus mode *
* can be found on GitHub at: *
* https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface#configuring-bus-mode *
*********************************************************************************/
Init()
{
thread Setup();
}
Setup()
{
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.overrideMethods[level.commonFunctions.getInboundData] = ::GetInboundData;
level.overrideMethods[level.commonFunctions.getOutboundData] = ::GetOutboundData;
level.overrideMethods[level.commonFunctions.setInboundData] = ::SetInboundData;
level.overrideMethods[level.commonFunctions.setOutboundData] = ::SetOutboundData;
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.busdir, GetDvar( "fs_homepath" ) );
}
GetInboundData( location )
{
return readFile( location );
}
GetOutboundData( location )
{
return readFile( location );
}
SetInboundData( location, data )
{
writeFile( location, data );
}
SetOutboundData( location, data )
{
writeFile( location, data );
}

View File

@ -1,93 +0,0 @@
Init()
{
level.startmessagedefaultduration = 2;
level.regulargamemessages = spawnstruct();
level.regulargamemessages.waittime = 6;
thread OnPlayerConnect();
}
OnPlayerConnect()
{
for ( ;; )
{
level waittill( "connecting", player );
player thread DisplayPopupsWaiter();
}
}
DisplayPopupsWaiter()
{
self endon( "disconnect" );
self.ranknotifyqueue = [];
if ( !IsDefined( self.pers[ "challengeNotifyQueue" ] ) )
{
self.pers[ "challengeNotifyQueue" ] = [];
}
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;
}
else
{
if ( self.startmessagenotifyqueue.size > 0 )
{
nextnotifydata = self.startmessagenotifyqueue[ 0 ];
arrayremoveindex( self.startmessagenotifyqueue, 0, 0 );
if ( IsDefined( nextnotifydata.duration ) )
{
duration = nextnotifydata.duration;
}
else
{
duration = level.startmessagedefaultduration;
}
self maps\mp\gametypes_zm\_hud_message::shownotifymessage( nextnotifydata, duration );
wait ( duration );
continue;
}
else if ( self.messagenotifyqueue.size > 0 )
{
nextnotifydata = self.messagenotifyqueue[ 0 ];
arrayremoveindex( self.messagenotifyqueue, 0, 0 );
if ( IsDefined( nextnotifydata.duration ) )
{
duration = nextnotifydata.duration;
}
else
{
duration = level.regulargamemessages.waittime;
}
self maps\mp\gametypes_zm\_hud_message::shownotifymessage( nextnotifydata, duration );
continue;
}
else
{
wait ( 1 );
}
}
}
}

View File

@ -1,40 +0,0 @@
/*
* This file contains reusable preprocessor directives meant to be used on
* Plutonium & AlterWare clients that are up to date with the latest version.
* Older versions of Plutonium or other clients do not have support for loading
* or parsing "gsh" files.
*/
/*
* Turn off assertions by removing the following define
* gsc-tool will only emit assertions if developer_script dvar is set to 1
* In short, you should not need to remove this define. Just turn them off
* by using the dvar
*/
#define _INTEGRATION_DEBUG
#ifdef _INTEGRATION_DEBUG
#define _VERIFY( cond, msg ) \
assertEx( cond, msg )
#else
// This works as an "empty" define here with gsc-tool
#define _VERIFY( cond, msg )
#endif
// This function is meant to be used inside "client commands"
// If the client is not alive it shall return an error message
#define _IS_ALIVE( ent ) \
_VERIFY( ent, "player entity is not defined" ); \
if ( !IsAlive( ent ) ) \
{ \
return ent.name + "^7 is not alive"; \
}
// This function should be used to verify if a player entity is defined
#define _VERIFY_PLAYER_ENT( ent ) \
_VERIFY( ent, "player entity is not defined" )

View File

@ -1,88 +0,0 @@
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] );
}
}

View File

@ -3,7 +3,14 @@
Allows integration of IW4M-Admin to GSC, mainly used for special commands that need to use GSC in order to work.
But can also be used to read / write metadata from / to a profile and to get the player permission level.
## Installation Guide
## Installation Plutonium IW5
The documentation can be found here: [GameInterface](https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface)
Move `_integration.gsc` to `%localappdata%\Plutonium\storage\iw5\scripts\`
## Installation IW4x
Move `_integration.gsc` to `IW4x/userraw/scripts`, `IW4x` being the root folder of your game server.

View File

@ -1,21 +1,14 @@
@echo off
ECHO "Pluto IW5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_utility.gsh "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
ECHO "Pluto T5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts"
xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
xcopy /y .\GameInterface\_integration_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
xcopy /y .\GameInterface\_integration_t5zm.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\sp\zom"
ECHO "Pluto T6"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts"
xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts"
xcopy /y .\GameInterface\_integration_t6.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts"
xcopy /y .\GameInterface\_integration_t6zm_helper.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\zm"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"

View File

@ -53,7 +53,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
Plugins\ScriptPlugins\ServerBanner.js = Plugins\ScriptPlugins\ServerBanner.js
Plugins\ScriptPlugins\ParserBOIII.js = Plugins\ScriptPlugins\ParserBOIII.js
Plugins\ScriptPlugins\ParserL4D2SM.js = Plugins\ScriptPlugins\ParserL4D2SM.js
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
@ -73,9 +72,6 @@ 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
@ -84,11 +80,6 @@ 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
GameFiles\GameInterface\_integration_t6_file_bus.gsc = GameFiles\GameInterface\_integration_t6_file_bus.gsc
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}"
@ -111,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto IW5", "Pluto IW5", "{
GameFiles\AntiCheat\IW5\README.MD = GameFiles\AntiCheat\IW5\README.MD
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZombieStats", "Plugins\ZombieStats\ZombieStats.csproj", "{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -439,6 +432,30 @@ Global
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|x64.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Debug|x86.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|x64.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Prerelease|x86.Build.0 = Debug|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|Any CPU.Build.0 = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|x64.ActiveCfg = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|x64.Build.0 = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|x86.ActiveCfg = Release|Any CPU
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -460,6 +477,7 @@ Global
{3EA564BD-3AC6-479B-96B6-CB059DCD0C77} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{866F453D-BC89-457F-8B55-485494759B31} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{603725A4-BC0B-423B-955B-762C89E1C4C2} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{3D92B950-6C9D-4B6D-B01B-F7AEC91787BD} = {26E8B310-269E-46D4-A612-24601F16065F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -8,7 +8,6 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Integrations.Cod.SecureRcon;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore;
@ -25,7 +24,6 @@ namespace Integrations.Cod
public class CodRConConnection : IRConConnection
{
private static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new();
private const string PkPattern = "-----BEGIN PRIVATE KEY-----";
public IPEndPoint Endpoint { get; }
public string RConPassword { get; }
@ -154,28 +152,32 @@ namespace Integrations.Cod
{
case StaticHelpers.QueryType.GET_DVAR:
waitForResponse = true;
payload = BuildPayload(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
convertedParameters);
payload = string
.Format(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.SET_DVAR:
payload = BuildPayload(_config.CommandPrefixes.RConSetDvar, convertedRConPassword,
convertedParameters);
payload = string
.Format(_config.CommandPrefixes.RConSetDvar, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND:
payload = BuildPayload(_config.CommandPrefixes.RConCommand, convertedRConPassword,
convertedParameters);
payload = string
.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_STATUS:
waitForResponse = true;
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Helpers.SafeConversion).ToArray();
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_INFO:
waitForResponse = true;
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Helpers.SafeConversion).ToArray();
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND_STATUS:
waitForResponse = true;
payload = BuildPayload(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status");
payload = string.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0")
.Select(Convert.ToByte).ToArray();
break;
}
}
@ -320,27 +322,6 @@ namespace Integrations.Cod
return validatedResponse;
}
private byte[] BuildPayload(string queryTemplate, string convertedRConPassword, string convertedParameters)
{
byte[] payload;
if (!RConPassword.StartsWith(PkPattern))
{
payload = string
.Format(queryTemplate, convertedRConPassword,
convertedParameters + '\0').Select(Helpers.SafeConversion).ToArray();
}
else
{
var textContent = string
.Format(queryTemplate, "", convertedParameters)
.Replace("rcon", "rconSafe ")
.Replace(" ", "").Split(" ");
payload = Helpers.BuildSafeRconPayload(textContent[0], textContent[1], RConPassword);
}
return payload;
}
private async Task<byte[][]> SendPayloadAsync(Socket rconSocket, byte[] payload, bool waitForResponse,
CancellationToken token = default)
{
@ -487,7 +468,7 @@ namespace Integrations.Cod
return string.Join("", splitStatusStrings);
}
/// <summary>
/// Recombines multiple game messages into one
/// </summary>
@ -520,7 +501,7 @@ namespace Integrations.Cod
{
return connectionState.ReceivedBytes.ToArray();
}
#endregion
}
}

View File

@ -16,8 +16,4 @@
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="protobuf-net" Version="3.2.26" />
</ItemGroup>
</Project>

View File

@ -1,57 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using ProtoBuf;
namespace Integrations.Cod.SecureRcon;
public static class Helpers
{
private static byte[] ToSerializedMessage(this SecureCommand command)
{
using var ms = new MemoryStream();
Serializer.Serialize(ms, command);
return ms.ToArray();
}
private static byte[] SignData(byte[] data, string privateKey)
{
using var rsa = new RSACryptoServiceProvider();
rsa.ImportFromPem(privateKey);
var rsaFormatter = new RSAPKCS1SignatureFormatter(rsa);
rsaFormatter.SetHashAlgorithm("SHA512");
var hash = SHA512.Create();
var hashedData = hash.ComputeHash(data);
var signature = rsaFormatter.CreateSignature(hashedData);
return signature;
}
public static byte SafeConversion(char c)
{
try
{
return Convert.ToByte(c);
}
catch
{
return (byte)'.';
}
}
public static byte[] BuildSafeRconPayload(string prefix, string command, string signingKey)
{
var message = command.Select(SafeConversion).ToArray();
var header = (prefix + "\n").Select(SafeConversion).ToArray();
var secureCommand = new SecureCommand
{
SecMessage = message,
Signature = SignData(message, signingKey)
};
return header.Concat(secureCommand.ToSerializedMessage()).ToArray();
}
}

View File

@ -1,13 +0,0 @@
using ProtoBuf;
namespace Integrations.Cod.SecureRcon;
[ProtoContract]
public class SecureCommand
{
[ProtoMember(1)]
public byte[] SecMessage { get; set; }
[ProtoMember(2)]
public byte[] Signature { get; set; }
}

View File

@ -104,7 +104,7 @@ namespace Integrations.Source
}
var split = response.TrimEnd('\n').Split('\n');
return split.Take(Math.Max(1, split.Length - 1)).ToArray();
return split.Take(split.Length - 1).ToArray();
}
catch (TaskCanceledException)

View File

@ -2,61 +2,34 @@
const inDvar = 'sv_iw4madmin_in';
const outDvar = 'sv_iw4madmin_out';
const integrationEnabledDvar = 'sv_iw4madmin_integration_enabled';
const groupSeparatorChar = '\x1d';
const recordSeparatorChar = '\x1e';
const unitSeparatorChar = '\x1f';
const pollingRate = 300;
let busFileIn = '';
let busFileOut = '';
let busMode = 'rcon';
let busDir = '';
const init = (registerNotify, serviceResolver, config, scriptHelper) => {
const init = (registerNotify, serviceResolver, config) => {
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, scriptHelper);
plugin.onLoad(serviceResolver, config);
return plugin;
};
const plugin = {
author: 'RaidMax',
version: '2.1',
version: '2.0',
name: 'Game Interface',
serviceResolver: null,
eventManager: null,
logger: null,
commands: null,
scriptHelper: null,
configWrapper: null,
config: {
pollingRate: 300
},
onLoad: function (serviceResolver, configWrapper, scriptHelper) {
onLoad: function (serviceResolver, config) {
this.serviceResolver = serviceResolver;
this.eventManager = serviceResolver.resolveService('IManager');
this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
this.commands = commands;
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);
}
this.config = config;
},
onClientEnteredMatch: function (clientEvent) {
@ -92,9 +65,6 @@ 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;
@ -117,22 +87,15 @@ const plugin = {
const input = serverState.inQueue.shift();
// if we queued an event then the next loop will be at the value set complete
await this.processEventMessage(input, serverValueEvent.server);
if (await this.processEventMessage(input, serverValueEvent.server)) {
// return;
}
}
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] = {
@ -162,12 +125,7 @@ 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);
},
@ -178,9 +136,7 @@ const plugin = {
const serverState = servers[responseEvent.server.id];
serverState.outQueue.shift();
const utilities = importNamespace('SharedLibraryCore.Utilities');
if (responseEvent.server.connectedClients.count === 0 && !utilities.isDevelopment) {
if (responseEvent.server.connectedClients.count === 0) {
// no clients connected so we don't need to query
serverState.running = false;
return;
@ -223,8 +179,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, event.clientNumber);
this.logger.logDebug('Processing input... {eventType} {subType} {data} {clientNumber}', event.eventType,
event.subType, event.data.toString(), event.clientNumber);
const metaService = this.serviceResolver.ResolveService('IMetaServiceV2');
const threading = importNamespace('System.Threading');
@ -252,7 +208,7 @@ const plugin = {
data = {
level: client.level,
clientId: client.clientId,
lastConnection: client.timeSinceLastConnectionString,
lastConnection: client.lastConnection,
tag: tagMeta?.value ?? '',
performance: clientStats?.performance ?? 200.0
};
@ -331,74 +287,17 @@ 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;
}
if (origin != null) {
originClientNumber = origin.clientNumber
}
const output = `${responseExpected ? '1' : '0'}${groupSeparatorChar}${event}${groupSeparatorChar}${subtype}${groupSeparatorChar}${originClientNumber}${groupSeparatorChar}${targetClientNumber}${groupSeparatorChar}${buildDataString(data)}`;
const output = `${responseExpected ? '1' : '0'};${event};${subtype};${origin.ClientNumber};${targetClientNumber};${buildDataString(data)}`;
this.logger.logDebug('Queuing output for server {output}', output);
servers[server.id].commandQueue.push(output);
@ -406,40 +305,9 @@ 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 = this.config.pollingRate;
requestEvent.delayMs = pollingRate;
requestEvent.timeoutMs = 2000;
requestEvent.source = this.name;
@ -449,7 +317,7 @@ const plugin = {
const diff = new Date().getTime() - end.getTime();
if (diff < extraDelay) {
requestEvent.delayMs = (extraDelay - diff) + this.config.pollingRate;
requestEvent.delayMs = (extraDelay - diff) + pollingRate;
this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs);
}
}
@ -467,39 +335,10 @@ 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 = this.config.pollingRate;
requestEvent.delayMs = pollingRate;
requestEvent.timeoutMs = 2000;
requestEvent.source = this.name;
@ -509,7 +348,7 @@ const plugin = {
const diff = new Date().getTime() - end.getTime();
if (diff < extraDelay) {
requestEvent.delayMs = (extraDelay - diff) + this.config.pollingRate;
requestEvent.delayMs = (extraDelay - diff) + pollingRate;
this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs);
}
}
@ -526,64 +365,8 @@ const plugin = {
}
},
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);
onServerMonitoringStart: function (monitorStartEvent) {
this.initializeServer(monitorStartEvent.server);
}
};
@ -602,7 +385,7 @@ const commands = [{
required: true
}
],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -622,7 +405,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -640,7 +423,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -658,7 +441,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -688,7 +471,7 @@ const commands = [{
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -711,7 +494,7 @@ const commands = [{
required: true
}
],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -732,7 +515,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -750,7 +533,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -777,7 +560,7 @@ const commands = [{
required: true
}
],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -801,7 +584,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -819,7 +602,7 @@ const commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
@ -851,7 +634,7 @@ const parseEvent = (input) => {
return {};
}
const eventInfo = input.split(groupSeparatorChar);
const eventInfo = input.split(';');
return {
eventType: eventInfo[1],
@ -869,7 +652,7 @@ const buildDataString = data => {
let formattedData = '';
for (let [key, value] of Object.entries(data)) {
formattedData += `${key}${unitSeparatorChar}${value}${recordSeparatorChar}`;
formattedData += `${key}=${value}|`;
}
return formattedData.slice(0, -1);
@ -881,11 +664,11 @@ const parseDataString = data => {
}
const dict = {};
const split = data.split(recordSeparatorChar);
const split = data.split('|');
for (let i = 0; i < split.length; i++) {
const segment = split[i];
const keyValue = segment.split(unitSeparatorChar);
const keyValue = segment.split('=');
if (keyValue.length !== 2) {
continue;
}
@ -906,20 +689,3 @@ 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;
}

View File

@ -22,10 +22,10 @@ const plugin = {
rconParser.Configuration.MapStatus.AddMapping(111, 1);
rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$';
rconParser.Configuration.HostnameStatus.AddMapping(113, 1);
rconParser.Configuration.MapStatus.AddMapping(113, 1);
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+';
rconParser.Configuration.MaxPlayersStatus.AddMapping(114, 1);
rconParser.Configuration.MapStatus.AddMapping(114, 1);
rconParser.Configuration.Dvar.Pattern = '^"(.+)" = "(.+)" (?:\\( def. "(.*)" \\))?(?: |\\w)+- (.+)$';
rconParser.Configuration.Dvar.AddMapping(106, 1);

View File

@ -3,7 +3,7 @@ let eventParser;
const plugin = {
author: 'RaidMax',
version: 0.7,
version: 0.6,
name: 'CS:GO (SourceMod) Parser',
engine: 'Source',
isParser: true,
@ -22,10 +22,10 @@ const plugin = {
rconParser.Configuration.MapStatus.AddMapping(111, 1);
rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$';
rconParser.Configuration.HostnameStatus.AddMapping(113, 1);
rconParser.Configuration.MapStatus.AddMapping(113, 1);
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+';
rconParser.Configuration.MaxPlayersStatus.AddMapping(114, 1);
rconParser.Configuration.MapStatus.AddMapping(114, 1);
rconParser.Configuration.Dvar.Pattern = '^"(.+)" = "(.+)" (?:\\( def. "(.*)" \\))?(?: |\\w)+- (.+)$';
rconParser.Configuration.Dvar.AddMapping(106, 1);

View File

@ -1,135 +0,0 @@
let rconParser;
let eventParser;
const plugin = {
author: 'RaidMax',
version: 0.1,
name: 'L4D2 (SourceMod) Parser',
engine: 'Source',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.RConEngine = this.engine;
rconParser.Configuration.StatusHeader.Pattern = '# userid name uniqueid connected ping loss state rate adr';
rconParser.Configuration.MapStatus.Pattern = '^map *: +(.+)$';
rconParser.Configuration.MapStatus.AddMapping(111, 1);
rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$';
rconParser.Configuration.HostnameStatus.AddMapping(113, 1);
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+';
rconParser.Configuration.MaxPlayersStatus.AddMapping(114, 1);
rconParser.Configuration.Dvar.Pattern = '^\\"(.+)\\" (?:=|is) \\"(.+)\\"(?: (?:\\( def. \\"(.*)\\" \\)))?$';
rconParser.Configuration.Dvar.AddMapping(106, 1);
rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.Dvar.AddMapping(108, 3);
rconParser.Configuration.Dvar.AddMapping(109, 3);
rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) +(\\d+:\\d+(?::\\d+)?) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$';
rconParser.Configuration.Status.AddMapping(100, 2);
rconParser.Configuration.Status.AddMapping(101, -1);
rconParser.Configuration.Status.AddMapping(102, 6);
rconParser.Configuration.Status.AddMapping(103, 4)
rconParser.Configuration.Status.AddMapping(104, 3);
rconParser.Configuration.Status.AddMapping(105, 10);
rconParser.Configuration.Status.AddMapping(200, 1);
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.DefaultDvarValues.Add('bugfix_no_version', this.engine);
rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basegame', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_homepath', '');
rconParser.Configuration.DefaultDvarValues.Add('g_log', '');
rconParser.Configuration.DefaultDvarValues.Add('net_ip', 'localhost');
rconParser.Configuration.DefaultDvarValues.Add('g_gametype', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_game', '');
rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'hostname');
rconParser.Configuration.OverrideDvarNameMapping.Add('mapname', 'host_map');
rconParser.Configuration.OverrideDvarNameMapping.Add('sv_maxclients', 'maxplayers');
rconParser.Configuration.OverrideDvarNameMapping.Add('g_password', 'sv_password');
rconParser.Configuration.OverrideDvarNameMapping.Add('version', 'bugfix_no_version');
rconParser.Configuration.ColorCodeMapping.Clear();
rconParser.Configuration.ColorCodeMapping.Add('White', '\x01');
rconParser.Configuration.ColorCodeMapping.Add('Red', '\x07');
rconParser.Configuration.ColorCodeMapping.Add('LightRed', '\x0F');
rconParser.Configuration.ColorCodeMapping.Add('DarkRed', '\x02');
rconParser.Configuration.ColorCodeMapping.Add('Blue', '\x0B');
rconParser.Configuration.ColorCodeMapping.Add('DarkBlue', '\x0C');
rconParser.Configuration.ColorCodeMapping.Add('Purple', '\x03');
rconParser.Configuration.ColorCodeMapping.Add('Orchid', '\x0E');
rconParser.Configuration.ColorCodeMapping.Add('Yellow', '\x09');
rconParser.Configuration.ColorCodeMapping.Add('Gold', '\x10');
rconParser.Configuration.ColorCodeMapping.Add('LightGreen', '\x05');
rconParser.Configuration.ColorCodeMapping.Add('Green', '\x04');
rconParser.Configuration.ColorCodeMapping.Add('Lime', '\x06');
rconParser.Configuration.ColorCodeMapping.Add('Grey', '\x08');
rconParser.Configuration.ColorCodeMapping.Add('Grey2', '\x0D');
// only adding there here for the default accent color
rconParser.Configuration.ColorCodeMapping.Add('Cyan', '\x0B');
rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.DefaultRConPort = 27015;
rconParser.CanGenerateLogPath = false;
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.CommandPrefixes.Kick = 'sm_kick #{0} {1}';
rconParser.Configuration.CommandPrefixes.Ban = 'sm_kick #{0} {1}';
rconParser.Configuration.CommandPrefixes.TempBan = 'sm_kick #{0} {1}';
rconParser.Configuration.CommandPrefixes.Say = 'sm_say {0}';
rconParser.Configuration.CommandPrefixes.Tell = 'sm_psay #{0} "{1}"';
eventParser.Configuration.Say.Pattern = '^"(.+)<(\\d+)><(.+)><(.*?)>" (?:say|say_team) "(.*)"$';
eventParser.Configuration.Say.AddMapping(5, 1);
eventParser.Configuration.Say.AddMapping(3, 2);
eventParser.Configuration.Say.AddMapping(1, 3);
eventParser.Configuration.Say.AddMapping(7, 4);
eventParser.Configuration.Say.AddMapping(13, 5);
eventParser.Configuration.Kill.Pattern = '^"(.+)<(\\d+)><(.+)><(.*)>" \\[-?\\d+ -?\\d+ -?\\d+\\] killed "(.+)<(\\d+)><(.+)><(.*)>" \\[-?\\d+ -?\\d+ -?\\d+\\] with "(\\S*)" *(?:\\((\\w+)((?: ).+)?\\))?$';
eventParser.Configuration.Kill.AddMapping(5, 1);
eventParser.Configuration.Kill.AddMapping(3, 2);
eventParser.Configuration.Kill.AddMapping(1, 3);
eventParser.Configuration.Kill.AddMapping(7, 4);
eventParser.Configuration.Kill.AddMapping(6, 5);
eventParser.Configuration.Kill.AddMapping(4, 6);
eventParser.Configuration.Kill.AddMapping(2, 7);
eventParser.Configuration.Kill.AddMapping(8, 8);
eventParser.Configuration.Kill.AddMapping(9, 9);
eventParser.Configuration.Kill.AddMapping(12, 10);
eventParser.Configuration.MapEnd.Pattern = '^World triggered "Match_Start" on "(.+)"$';
eventParser.Configuration.JoinTeam.Pattern = '^"(.+)<(\\d+)><(.*)>" switched from team <(.+)> to <(.+)>$';
eventParser.Configuration.JoinTeam.AddMapping(5, 1);
eventParser.Configuration.JoinTeam.AddMapping(3, 2);
eventParser.Configuration.JoinTeam.AddMapping(1, 3);
eventParser.Configuration.JoinTeam.AddMapping(7, 5);
eventParser.Configuration.TeamMapping.Add('CT', 2);
eventParser.Configuration.TeamMapping.Add('TERRORIST', 3);
eventParser.Configuration.Time.Pattern = '^L [01]\\d/[0-3]\\d/\\d+ - [0-2]\\d:[0-5]\\d:[0-5]\\d:';
rconParser.Version = 'L4D2SM';
rconParser.GameName = 12; // L4D2
eventParser.Version = 'L4D2SM';
eventParser.GameName = 12; // L4D2
eventParser.URLProtocolFormat = 'steam://connect/{{ip}}:{{port}}';
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -5,7 +5,7 @@ namespace Stats.Client.Abstractions
public interface IServerDistributionCalculator
{
Task Initialize();
Task<double> GetZScoreForServer(long serverId, double value);
Task<double?> GetRatingForZScore(double? value);
Task<double> GetZScoreForServerOrBucket(double value, long? serverId = null, string performanceBucket = null);
Task<double?> GetRatingForZScore(double? value, string performanceBucket);
}
}
}

View File

@ -181,7 +181,7 @@ public class HitCalculator : IClientStatisticCalculator
foreach (var hitInfo in new[] {attackerHitInfo, victimHitInfo})
{
if (hitInfo.MeansOfDeath == null || hitInfo.Location == null || hitInfo.Weapon == null)
if (hitInfo.MeansOfDeath == null || hitInfo.Location == null || hitInfo.Weapon == null || hitInfo.EntityId == 0)
{
_logger.LogDebug("Skipping hit because it does not contain the required data");
continue;

View File

@ -7,10 +7,9 @@ using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats;
using IW4MAdmin.Plugins.Stats.Config;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Configuration;
using Stats.Client.Abstractions;
using Stats.Config;
using Stats.Helpers;
@ -21,76 +20,119 @@ namespace Stats.Client
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly IDataValueCache<EFClientStatistics, Dictionary<long, Extensions.LogParams>>
private readonly IDataValueCache<EFClientStatistics, Dictionary<string, Extensions.LogParams>>
_distributionCache;
private readonly IDataValueCache<EFClientStatistics, double>
_maxZScoreCache;
private readonly IDataValueCache<EFClientStatistics, double> _maxZScoreCache;
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
private readonly List<long> _serverIds = new List<long>();
private readonly StatsConfiguration _configuration;
private readonly ApplicationConfiguration _appConfig;
private readonly List<long> _serverIds = new();
private const string DistributionCacheKey = nameof(DistributionCacheKey);
private const string MaxZScoreCacheKey = nameof(MaxZScoreCacheKey);
public ServerDistributionCalculator(IDatabaseContextFactory contextFactory,
IDataValueCache<EFClientStatistics, Dictionary<long, Extensions.LogParams>> distributionCache,
IDataValueCache<EFClientStatistics, Dictionary<string, Extensions.LogParams>> distributionCache,
IDataValueCache<EFClientStatistics, double> maxZScoreCache,
IConfigurationHandlerFactory configFactory)
StatsConfiguration config, ApplicationConfiguration appConfig)
{
_contextFactory = contextFactory;
_distributionCache = distributionCache;
_maxZScoreCache = maxZScoreCache;
_configurationHandler = configFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_configuration = config;
_appConfig = appConfig;
}
public async Task Initialize()
{
await LoadServers();
_distributionCache.SetCacheItem((async (set, token) =>
_distributionCache.SetCacheItem(async (set, token) =>
{
await _configurationHandler.BuildAsync();
var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3;
var distributions = new Dictionary<long, Extensions.LogParams>();
var validPlayTime = _configuration.TopPlayersMinPlayTime;
var distributions = new Dictionary<string, Extensions.LogParams>();
await LoadServers();
var iqPerformances = set
.Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0)
.Where(s => s.Client.Level != EFClient.Permission.Banned);
foreach (var serverId in _serverIds)
{
var performance = await set
.Where(s => s.ServerId == serverId)
.Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0)
.Where(s => s.Client.Level != EFClient.Permission.Banned)
var performances = await iqPerformances.Where(s => s.ServerId == serverId)
.Where(s => s.TimePlayed >= validPlayTime)
.Where(s => s.UpdatedAt >= Extensions.FifteenDaysAgo())
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0).ToListAsync();
var distributionParams = performance.GenerateDistributionParameters();
distributions.Add(serverId, distributionParams);
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
.ToListAsync(token);
var distributionParams = performances.GenerateDistributionParameters();
distributions.Add(serverId.ToString(), distributionParams);
}
foreach (var server in _appConfig.Servers)
{
if (string.IsNullOrWhiteSpace(server.PerformanceBucket))
{
continue;
}
var bucketConfig =
_configuration.PerformanceBuckets.FirstOrDefault(bucket =>
bucket.Name == server.PerformanceBucket) ?? new PerformanceBucketConfiguration();
var oldestPerf = DateTimeOffset.UtcNow - bucketConfig.RankingExpiration;
var performances = await iqPerformances
.Where(perf => perf.Server.PerformanceBucket == server.PerformanceBucket)
.Where(perf => perf.TimePlayed >= bucketConfig.ClientMinPlayTime.TotalSeconds)
.Where(perf => perf.UpdatedAt >= oldestPerf)
.Select(s => s.EloRating * 1 / 3.0 + s.Skill * 2 / 3.0)
.ToListAsync(token);
var distributionParams = performances.GenerateDistributionParameters();
distributions.Add(server.PerformanceBucket, distributionParams);
}
return distributions;
}), DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1));
}, DistributionCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromHours(1));
_maxZScoreCache.SetCacheItem(async (set, token) =>
foreach (var server in _appConfig.Servers)
{
await _configurationHandler.BuildAsync();
var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3;
_maxZScoreCache.SetCacheItem(async (set, ids, token) =>
{
var validPlayTime = _configuration.TopPlayersMinPlayTime;
var oldestStat = TimeSpan.FromSeconds(_configuration.TopPlayersMinPlayTime);
var performanceBucket = (string)ids.FirstOrDefault();
var zScore = await set
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime))
.Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0)
.GroupBy(stat => stat.ClientId)
.Select(group =>
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed))
.MaxAsync(avgZScore => (double?) avgZScore, token);
return zScore ?? 0;
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
if (!string.IsNullOrEmpty(performanceBucket))
{
var bucketConfig =
_configuration.PerformanceBuckets.FirstOrDefault(cfg =>
cfg.Name == performanceBucket) ?? new PerformanceBucketConfiguration();
validPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
oldestStat = bucketConfig.RankingExpiration;
}
var zScore = await set
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(validPlayTime, oldestStat))
.Where(s => s.Skill > 0)
.Where(s => s.EloRating > 0)
.Where(stat =>
performanceBucket == null || performanceBucket == stat.Server.PerformanceBucket)
.GroupBy(stat => stat.ClientId)
.Select(group =>
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed))
.MaxAsync(avgZScore => (double?)avgZScore, token);
return zScore ?? 0;
}, MaxZScoreCacheKey, new[] { server.PerformanceBucket },
Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new[] { server.PerformanceBucket });
}
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
/*foreach (var serverId in _serverIds)
{
@ -131,16 +173,28 @@ namespace Stats.Client
}
}
public async Task<double> GetZScoreForServer(long serverId, double value)
public async Task<double> GetZScoreForServerOrBucket(double value, long? serverId = null,
string performanceBucket = null)
{
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
if (!serverParams.ContainsKey(serverId))
if (serverId is null && performanceBucket is null)
{
return 0.0;
}
var sdParams = serverParams[serverId];
if (sdParams.Sigma == 0)
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
Extensions.LogParams sdParams = null;
if (serverId is not null && serverParams.TryGetValue(serverId.ToString(), out var sdParams1))
{
sdParams = sdParams1;
}
else if (performanceBucket is not null && serverParams.TryGetValue(performanceBucket, out var sdParams2))
{
sdParams = sdParams2;
}
if (sdParams is null || sdParams.Sigma == 0)
{
return 0.0;
}
@ -149,9 +203,9 @@ namespace Stats.Client
return zScore;
}
public async Task<double?> GetRatingForZScore(double? value)
public async Task<double?> GetRatingForZScore(double? value, string performanceBucket)
{
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new [] { performanceBucket });
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Stats.Config;
public class PerformanceBucketConfiguration
{
public string Name { get; set; }
public TimeSpan ClientMinPlayTime { get; set; } = TimeSpan.FromHours(3);
public TimeSpan RankingExpiration { get; set; } = TimeSpan.FromDays(15);
}

View File

@ -19,6 +19,8 @@ namespace Stats.Config
public int MostKillsClientLimit { get; set; } = 5;
public bool EnableAdvancedMetrics { get; set; } = true;
public List<PerformanceBucketConfiguration> PerformanceBuckets { get; set; } = new();
public WeaponNameParserConfiguration[] WeaponNameParserConfigurations { get; set; } = {
new()
{

View File

@ -25,25 +25,11 @@ namespace Stats.Dtos
/// </summary>
public DateTime? SentAfter { get; set; }
/// <summary>
/// The time associated with SentAfter date
/// </summary>
public string SentAfterTime { get; set; }
public DateTime? SentAfterDateTime => SentAfter?.Add(string.IsNullOrEmpty(SentAfterTime) ? TimeSpan.Zero : TimeSpan.Parse(SentAfterTime));
/// <summary>
/// only look for messages sent before this date0
/// </summary>
public DateTime SentBefore { get; set; } = DateTime.UtcNow.Date;
public DateTime SentBefore { get; set; } = DateTime.UtcNow;
public string SentBeforeTime { get; set; }
public DateTime? SentBeforeDateTime =>
SentBefore.Add(string.IsNullOrEmpty(SentBeforeTime) ? TimeSpan.Zero : TimeSpan.Parse(SentBeforeTime));
public bool IsExactMatch { get; set; }
/// <summary>
/// indicates if the chat is on the meta page
/// </summary>

View File

@ -13,6 +13,7 @@ namespace IW4MAdmin.Plugins.Stats
{
private const int ZScoreRange = 3;
private const int RankIconDivisions = 24;
private const int MaxMessages = 100;
public class LogParams
{
@ -126,5 +127,70 @@ namespace IW4MAdmin.Plugins.Stats
return 0;
}
/// <summary>
/// todo: lets abstract this out to a generic buildable query
/// this is just a dirty PoC
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
public static ChatSearchQuery ParseSearchInfo(this string query, int count, int offset)
{
string[] filters = query.Split('|');
var searchRequest = new ChatSearchQuery
{
Filter = query,
Count = count,
Offset = offset
};
// sanity checks
searchRequest.Count = Math.Min(searchRequest.Count, MaxMessages);
searchRequest.Count = Math.Max(searchRequest.Count, 0);
searchRequest.Offset = Math.Max(searchRequest.Offset, 0);
if (filters.Length > 1)
{
if (filters[0].ToLower() != "chat")
{
throw new ArgumentException("Query is not compatible with chat");
}
foreach (string filter in filters.Skip(1))
{
string[] args = filter.Split(' ');
if (args.Length > 1)
{
string recombinedArgs = string.Join(' ', args.Skip(1));
switch (args[0].ToLower())
{
case "before":
searchRequest.SentBefore = DateTime.Parse(recombinedArgs);
break;
case "after":
searchRequest.SentAfter = DateTime.Parse(recombinedArgs);
break;
case "server":
searchRequest.ServerId = args[1];
break;
case "client":
searchRequest.ClientId = int.Parse(args[1]);
break;
case "contains":
searchRequest.MessageContains = string.Join(' ', args.Skip(1));
break;
case "sort":
searchRequest.Direction = Enum.Parse<SortDirection>(args[1], ignoreCase: true);
break;
}
}
}
return searchRequest;
}
throw new ArgumentException("No filters specified for chat search");
}
}
}

View File

@ -145,11 +145,12 @@ namespace Stats.Helpers
};
}
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, double? zScore = null,
public static Expression<Func<EFClientStatistics, bool>> GetRankingFunc(int minPlayTime, TimeSpan expiration, double? zScore = null,
long? serverId = null)
{
return (stats) => (serverId == null || stats.ServerId == serverId) &&
stats.UpdatedAt >= Extensions.FifteenDaysAgo() &&
var oldestStat = DateTime.UtcNow.Subtract(expiration);
return stats => (serverId == null || stats.ServerId == serverId) &&
stats.UpdatedAt >= oldestStat &&
stats.Client.Level != EFClient.Permission.Banned &&
stats.TimePlayed >= minPlayTime
&& (zScore == null || stats.ZScore > zScore);

View File

@ -53,11 +53,11 @@ namespace Stats.Helpers
}
var iqMessages = context.Set<EFClientMessage>()
.Where(message => message.TimeSent < query.SentBeforeDateTime);
.Where(message => message.TimeSent < query.SentBefore);
if (query.SentAfterDateTime is not null)
if (query.SentAfter is not null)
{
iqMessages = iqMessages.Where(message => message.TimeSent >= query.SentAfterDateTime);
iqMessages = iqMessages.Where(message => message.TimeSent >= query.SentAfter);
}
if (query.ClientId is not null)
@ -72,10 +72,7 @@ namespace Stats.Helpers
if (!string.IsNullOrEmpty(query.MessageContains))
{
iqMessages = query.IsExactMatch
? iqMessages.Where(message => message.Message.ToLower() == query.MessageContains.ToLower())
: iqMessages.Where(message =>
EF.Functions.Like(message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
iqMessages = iqMessages.Where(message => EF.Functions.Like(message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
}
var iqResponse = iqMessages

View File

@ -1,5 +1,4 @@
using IW4MAdmin.Plugins.Stats.Cheat;
using IW4MAdmin.Plugins.Stats.Config;
using IW4MAdmin.Plugins.Stats.Web.Dtos;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
@ -1185,93 +1184,123 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
await using var context = _contextFactory.CreateContext();
var minPlayTime = _config.TopPlayersMinPlayTime;
var oldestStat = DateTimeOffset.UtcNow - Extensions.FifteenDaysAgo();
var performanceBucket =
(await _serverCache.FirstAsync(server => server.Id == serverId)).PerformanceBucket;
if (!string.IsNullOrEmpty(performanceBucket))
{
var bucketConfig = _config.PerformanceBuckets.FirstOrDefault(cfg => cfg.Name == performanceBucket) ??
new PerformanceBucketConfiguration();
minPlayTime = (int)bucketConfig.ClientMinPlayTime.TotalSeconds;
oldestStat = bucketConfig.RankingExpiration;
}
var oldestStateDate = DateTime.UtcNow - oldestStat;
var performances = await context.Set<EFClientStatistics>()
.AsNoTracking()
.Where(stat => stat.ClientId == clientId)
.Where(stat => stat.ServerId != serverId) // ignore the one we're currently tracking
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
.Where(stats => stats.UpdatedAt >= oldestStateDate)
.Where(stats => stats.TimePlayed >= minPlayTime)
.ToListAsync();
if (clientStats.TimePlayed >= minPlayTime)
{
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
clientStats.Performance);
var serverRanking = await context.Set<EFClientStatistics>()
.Where(stats => stats.ClientId != clientStats.ClientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(
_config.TopPlayersMinPlayTime, clientStats.ZScore, serverId))
.CountAsync();
var serverRankingSnapshot = new EFClientRankingHistory
{
ClientId = clientId,
ServerId = serverId,
ZScore = clientStats.ZScore,
Ranking = serverRanking,
PerformanceMetric = clientStats.Performance,
Newest = true
};
context.Add(serverRankingSnapshot);
await PruneOldRankings(context, clientId, serverId);
await context.SaveChangesAsync();
await UpdateForServer(clientId, clientStats, context, minPlayTime, oldestStat, serverId);
performances.Add(clientStats);
}
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
{
var aggregateZScore =
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
int? aggregateRanking = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId != clientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime))
.GroupBy(stat => stat.ClientId)
.Where(group =>
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) >
aggregateZScore)
.Select(c => c.Key)
.CountAsync();
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore);
if (newPerformanceMetric == null)
{
_log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}",
clientStats.Client?.ToString(), aggregateZScore);
return;
}
var aggregateRankingSnapshot = new EFClientRankingHistory
{
ClientId = clientId,
ZScore = aggregateZScore,
Ranking = aggregateRanking,
PerformanceMetric = newPerformanceMetric,
Newest = true,
};
context.Add(aggregateRankingSnapshot);
await PruneOldRankings(context, clientId);
await context.SaveChangesAsync();
await UpdateAggregateForServerOrBucket(clientId, clientStats, context, performances, minPlayTime,
oldestStat, performanceBucket);
}
}
private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null)
private async Task UpdateAggregateForServerOrBucket(int clientId, EFClientStatistics clientStats, DatabaseContext context, List<EFClientStatistics> performances,
int minPlayTime, TimeSpan oldestStat, string performanceBucket)
{
var aggregateZScore =
performances.Where(performance => performance.Server.PerformanceBucket == performanceBucket)
.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
int? aggregateRanking = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId != clientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat))
.GroupBy(stat => stat.ClientId)
.Where(group =>
group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) >
aggregateZScore)
.Select(c => c.Key)
.CountAsync();
var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore, performanceBucket);
if (newPerformanceMetric == null)
{
_log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}",
clientStats.Client?.ToString(), aggregateZScore);
return;
}
var aggregateRankingSnapshot = new EFClientRankingHistory
{
ClientId = clientId,
ZScore = aggregateZScore,
Ranking = aggregateRanking,
PerformanceMetric = newPerformanceMetric,
PerformanceBucket = performanceBucket,
Newest = true,
};
context.Add(aggregateRankingSnapshot);
await PruneOldRankings(context, clientId);
await context.SaveChangesAsync();
}
private async Task UpdateForServer(int clientId, EFClientStatistics clientStats, DatabaseContext context,
int minPlayTime, TimeSpan oldestStat, long? serverId = null)
{
clientStats.ZScore =
await _serverDistributionCalculator.GetZScoreForServerOrBucket(clientStats.Performance, serverId);
var serverRanking = await context.Set<EFClientStatistics>()
.Where(stats => stats.ClientId != clientStats.ClientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime, oldestStat,
clientStats.ZScore, serverId))
.CountAsync();
var serverRankingSnapshot = new EFClientRankingHistory
{
ClientId = clientId,
ServerId = serverId,
ZScore = clientStats.ZScore,
Ranking = serverRanking,
PerformanceMetric = clientStats.Performance,
Newest = true
};
context.Add(serverRankingSnapshot);
await PruneOldRankings(context, clientId, serverId);
await context.SaveChangesAsync();
}
private async Task PruneOldRankings(DatabaseContext context, int clientId, long? serverId = null, string performanceBucket = null)
{
var totalRankingEntries = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId)
.Where(r => r.PerformanceBucket == performanceBucket)
.CountAsync();
var mostRecent = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId)
.Where(r => r.PerformanceBucket == performanceBucket)
.FirstOrDefaultAsync(r => r.Newest);
if (mostRecent != null)
@ -1287,6 +1316,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var lastRating = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId)
.Where(r => r.PerformanceBucket == performanceBucket)
.OrderBy(r => r.CreatedDateTime)
.FirstOrDefaultAsync();
@ -1413,7 +1443,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
clientStats.SPM = Math.Round(clientStats.SPM, 3);
clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3);
var skillFunction = client.GetAdditionalProperty<Func<EFClient, EFClientStatistics, double>>("SkillFunction");
clientStats.Skill =
skillFunction?.Invoke(client, clientStats) ?? Math.Round(clientStats.SPM * KDRWeight, 3);
// fixme: how does this happen?
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
@ -1424,7 +1457,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
});
clientStats.SPM = 0;
clientStats.Skill = 0;
if (skillFunction is null)
{
clientStats.Skill = 0;
}
}
clientStats.LastStatCalculation = DateTime.UtcNow;

View File

@ -469,8 +469,6 @@ public class Plugin : IPluginV2
ClientId = request.ClientId,
Before = request.Before,
SentBefore = request.Before ?? DateTime.UtcNow,
SentAfter = request.After,
After = request.After,
Count = request.Count,
IsProfileMeta = true
};

View File

@ -0,0 +1 @@
// this component is closed source

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>IW4MAdmin.Plugins.ZombieStats</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.16">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!--<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.15.3" />-->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

@ -56,4 +56,4 @@ Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3)
If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues)
#### Explore the [wiki](https://git.rimmyscorner.com/Parasyn/IW4M-Admin/wiki) to find more information.
#### Explore the [wiki](https://github.com/RaidMax/IW4M-Admin/wiki) to find more information.

View File

@ -178,8 +178,7 @@ namespace SharedLibraryCore
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
ViewBag.PermissionsSet = PermissionsSet;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client);
ViewBag.Manager = Manager;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
base.OnActionExecuting(context);
}

View File

@ -206,7 +206,7 @@ namespace SharedLibraryCore.Configuration
: ManualWebfrontUrl;
[ConfigurationIgnore] public bool IgnoreServerConnectionLost { get; set; }
[ConfigurationIgnore] public Uri MasterUrl { get; set; } = new("https://master.iw4.zip");
[ConfigurationIgnore] public Uri MasterUrl { get; set; } = new("http://api.raidmax.org:5000");
public IBaseConfiguration Generate()
{

View File

@ -54,6 +54,7 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_CUSTOM_HOSTNAME")]
[ConfigurationOptional]
public string CustomHostname { get; set; }
public string PerformanceBucket { get; set; }
public IBaseConfiguration Generate()
{

View File

@ -67,7 +67,7 @@ namespace SharedLibraryCore.Configuration.Validation
RuleFor(_app => _app.MasterUrl)
.NotNull()
.Must(_url => _url != null && (_url.Scheme == Uri.UriSchemeHttp || _url.Scheme == Uri.UriSchemeHttps));
.Must(_url => _url != null && _url.Scheme == Uri.UriSchemeHttp);
RuleFor(_app => _app.CommandPrefix)
.NotEmpty();
@ -80,4 +80,4 @@ namespace SharedLibraryCore.Configuration.Validation
.SetValidator(new ServerConfigurationValidator());
}
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace SharedLibraryCore.Dtos
{
@ -12,17 +11,11 @@ namespace SharedLibraryCore.Dtos
public class ClientCountSnapshot
{
[JsonIgnore]
public DateTime Time { get; set; }
[JsonPropertyName("ts")]
public string TimeString => Time.ToString("yyyy-MM-ddTHH:mm:ssZ");
[JsonPropertyName("cc")]
public int ClientCount { get; set; }
[JsonPropertyName("ci")]
public bool ConnectionInterrupted { get;set; }
[JsonIgnore]
public string Map { get; set; }
[JsonPropertyName("ma")]
public string MapAlias { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using Data.Models.Client;
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerConsumedPerkGameEvent : ClientGameEvent
{
public EFClient Consumer => Origin;
public string PerkName { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerDamageGameEvent : ClientDamageEvent
{
}

View File

@ -0,0 +1,6 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerDownedGameEvent : ClientGameEvent
{
}

View File

@ -0,0 +1,9 @@
using Data.Models.Client;
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerGrabbedPowerupGameEvent : ClientGameEvent
{
public EFClient Grabber => Origin;
public string PowerupName { get; init; }
}

View File

@ -0,0 +1,9 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerKilledGameEvent : PlayerDamageGameEvent
{
public PlayerKilledGameEvent()
{
RequiredEntity = EventRequiredEntity.Target;
}
}

View File

@ -0,0 +1,9 @@
using Data.Models.Client;
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerRevivedGameEvent : ClientGameEvent
{
public EFClient Reviver => Origin;
public EFClient Revived => Target;
}

View File

@ -0,0 +1,8 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class PlayerRoundDataGameEvent : ClientGameEvent
{
public int TotalScore { get; init; }
public int CurrentScore { get; init; }
public bool IsGameOver { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class RoundCompleteGameEvent : GameEventV2
{
public int RoundNumber { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class ZombieDamageGameEvent : ClientDamageEvent
{
}

View File

@ -0,0 +1,9 @@
namespace SharedLibraryCore.Events.Game.GameScript.Zombie;
public class ZombieKilledGameEvent : ZombieDamageGameEvent
{
public ZombieKilledGameEvent()
{
RequiredEntity = EventRequiredEntity.Origin;
}
}

View File

@ -117,9 +117,6 @@ 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

View File

@ -35,8 +35,7 @@ namespace SharedLibraryCore
T7 = 8,
SHG1 = 9,
CSGO = 10,
H1 = 11,
L4D2 = 12
H1 = 11
}
// only here for performance

View File

@ -215,8 +215,7 @@ namespace SharedLibraryCore.Services
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
}
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, Reference.Game game, int? ipAddress = null,
EFPenalty.PenaltyType[] penaltyTypes = null)
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, Reference.Game game, int? ipAddress = null)
{
await using var context = _contextFactory.CreateContext();
var now = DateTime.UtcNow;
@ -227,7 +226,6 @@ namespace SharedLibraryCore.Services
{
var ids = activePenalties.Select(penalty => penalty.PenaltyId);
await context.Penalties.Where(penalty => ids.Contains(penalty.PenaltyId))
.Where(pen => penaltyTypes == null || penaltyTypes.Contains(pen.Type))
.ForEachAsync(penalty =>
{
penalty.Active = false;

View File

@ -50,8 +50,8 @@ namespace SharedLibraryCore
public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!';
public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd HH:mm:ss UTC");
public static string ToStandardFormat(this DateTime time) => time.ToString("yyyy-MM-dd HH:mm:ss UTC");
public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd H:mm:ss UTC");
public static string ToStandardFormat(this DateTime time) => time.ToString("yyyy-MM-dd H:mm:ss UTC");
public static EFClient IW4MAdminClient(Server server = null)
{
@ -195,7 +195,7 @@ namespace SharedLibraryCore
}
var output = str;
var colorCodeMatches = Regex.Matches(output, @"\(Color::(\w{1,16})\)",
var colorCodeMatches = Regex.Matches(output, @"\(Color::(.{1,16})\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
foreach (var match in colorCodeMatches.Where(m => m.Success))
{
@ -1145,7 +1145,7 @@ namespace SharedLibraryCore
/// </summary>
/// <returns></returns>
public static bool IsDevelopment =>
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" || AppContext.TryGetSwitch("IsDevelop", out _);
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
/// <summary>
/// replaces any directory separator chars with the platform specific character

View File

@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Controllers.API.Models;
@ -16,14 +12,9 @@ namespace WebfrontCore.Controllers.API
[Route("api/[controller]")]
public class Server : BaseController
{
private readonly IServerDataViewer _serverDataViewer;
private readonly ApplicationConfiguration _applicationConfiguration;
public Server(IManager manager, IServerDataViewer serverDataViewer,
ApplicationConfiguration applicationConfiguration) : base(manager)
public Server(IManager manager) : base(manager)
{
_serverDataViewer = serverDataViewer;
_applicationConfiguration = applicationConfiguration;
}
[HttpGet]
@ -119,48 +110,5 @@ namespace WebfrontCore.Controllers.API
completedEvent.Output
});
}
[HttpGet("{id}/history")]
public async Task<IActionResult> GetClientHistory(string id)
{
var foundServer = Manager.GetServers().FirstOrDefault(server => server.Id == id);
if (foundServer == null)
{
return new NotFoundResult();
}
var clientHistory = (await _serverDataViewer.ClientHistoryAsync(_applicationConfiguration.MaxClientHistoryTime,
CancellationToken.None))?
.FirstOrDefault(history => history.ServerId == foundServer.LegacyDatabaseId) ??
new ClientHistoryInfo
{
ServerId = foundServer.LegacyDatabaseId,
ClientCounts = new List<ClientCountSnapshot>()
};
var counts = clientHistory.ClientCounts?.AsEnumerable() ?? Enumerable.Empty<ClientCountSnapshot>();
if (foundServer.ClientHistory.ClientCounts.Any())
{
counts = counts.Union(foundServer.ClientHistory.ClientCounts.Where(history =>
history.Time > (clientHistory.ClientCounts?.LastOrDefault()?.Time ?? DateTime.MinValue)))
.Where(history => history.Time >= DateTime.UtcNow - _applicationConfiguration.MaxClientHistoryTime);
}
if (ViewBag.Maps?.Count == 0)
{
return Json(counts.ToList());
}
var clientCountSnapshots = counts.ToList();
foreach (var count in clientCountSnapshots)
{
count.MapAlias = foundServer.Maps.FirstOrDefault(map => map.Name == count.Map)?.Alias ??
count.Map;
}
return Json(clientCountSnapshots);
}
}
}

View File

@ -249,8 +249,7 @@ namespace WebfrontCore.Controllers
{
ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"];
ViewBag.ClientResourceRequest = request;
request.RequesterPermission = Client.Level;
var response = await _clientResourceHelper.QueryResource(request);
return request.Offset > 0
? PartialView("Find/_AdvancedFindList", response.Results)

View File

@ -17,7 +17,6 @@ using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Abstractions;
using Stats.Config;
using WebfrontCore.QueryHelpers.Models;
namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
{
@ -122,7 +121,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
}
[HttpGet("Message/Find")]
public async Task<IActionResult> FindMessage([FromQuery] ChatResourceRequest query)
public async Task<IActionResult> FindMessage([FromQuery] string query)
{
ViewBag.Localization = _translationLookup;
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
@ -131,15 +130,53 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
ViewBag.Title = _translationLookup["WEBFRONT_STATS_MESSAGES_TITLE"];
ViewBag.Error = null;
ViewBag.IsFluid = true;
var result = query != null ? await _chatResourceQueryHelper.QueryResource(query) : null;
ChatSearchQuery searchRequest = null;
try
{
searchRequest = query.ParseSearchInfo(int.MaxValue, 0);
}
catch (ArgumentException e)
{
_logger.LogWarning(e, "Could not parse chat message search query {query}", query);
ViewBag.Error = e;
}
catch (FormatException e)
{
_logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
ViewBag.Error = e;
}
var result = searchRequest != null ? await _chatResourceQueryHelper.QueryResource(searchRequest) : null;
return View("~/Views/Client/Message/Find.cshtml", result);
}
[HttpGet("Message/FindNext")]
public async Task<IActionResult> FindNextMessages(ChatResourceRequest query)
public async Task<IActionResult> FindNextMessages([FromQuery] string query, [FromQuery] int count,
[FromQuery] int offset)
{
var result = await _chatResourceQueryHelper.QueryResource(query);
ChatSearchQuery searchRequest;
try
{
searchRequest = query.ParseSearchInfo(count, offset);
}
catch (ArgumentException e)
{
_logger.LogWarning(e, "Could not parse chat message search query {query}", query);
throw;
}
catch (FormatException e)
{
_logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
throw;
}
var result = await _chatResourceQueryHelper.QueryResource(searchRequest);
return PartialView("~/Views/Client/Message/_Item.cshtml", result.Results);
}

View File

@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace WebfrontCore.Controllers
{
[Route("dynamic")]
public class DynamicFileController : BaseController
{
private static readonly IDictionary<string, string> _fileCache = new Dictionary<string, string>();
public DynamicFileController(IManager manager) : base(manager)
{
}
[Route("css/{fileName}")]
public async Task<IActionResult> Css(string fileName)
{
if (fileName.EndsWith(".css"))
{
if (Utilities.IsDevelopment)
{
var path = Path.Join(Utilities.OperatingDirectory, "..", "..", "..", "..", "WebfrontCore", "wwwroot", "css", fileName);
string cssData = await System.IO.File.ReadAllTextAsync(path);
cssData = await Manager.MiddlewareActionHandler.Execute(cssData, "custom_css_accent");
return Content(cssData, "text/css");
}
if (!_fileCache.ContainsKey(fileName))
{
string path = $"wwwroot{Path.DirectorySeparatorChar}css{Path.DirectorySeparatorChar}{fileName}";
string data = await System.IO.File.ReadAllTextAsync(path);
data = await Manager.MiddlewareActionHandler.Execute(data, "custom_css_accent");
_fileCache.Add(fileName, data);
}
return Content(_fileCache[fileName], "text/css");
}
return StatusCode(400);
}
}
}

Some files were not shown because too many files have changed in this diff Show More