You've already forked IW4M-Admin
Compare commits
263 Commits
2.3-prerel
...
2.3-Prerel
Author | SHA1 | Date | |
---|---|---|---|
9dfdf5a82b | |||
f5b0167f81 | |||
7715113b56 | |||
58bfd189d0 | |||
3645cf53ff | |||
8a98ed7c50 | |||
5529858edd | |||
ff011be8a6 | |||
b41c4c6245 | |||
92a26600af | |||
9e74dac5ed | |||
3ae2e42718 | |||
0b643b2099 | |||
ee087f1c85 | |||
8c29027b3f | |||
5bc1ad5926 | |||
c376266090 | |||
8539223a15 | |||
b188e36786 | |||
fca47cbce0 | |||
be8041b868 | |||
b63d2995ed | |||
8fb2394130 | |||
36af673fc7 | |||
9fdf4bad9c | |||
02a784ad09 | |||
2e5ffe91fc | |||
68490bde57 | |||
f430dab3a7 | |||
c3c21a7749 | |||
ec053eb854 | |||
33494197e3 | |||
239ca30fd1 | |||
1dd88cdacb | |||
f0f9a6beda | |||
fe380ca331 | |||
15e2170100 | |||
2872d02c37 | |||
60ff33834e | |||
06cdaef8a4 | |||
c6d6bebeab | |||
31c259f966 | |||
318a23ae5b | |||
11ae91281f | |||
1fd31beb05 | |||
116c909c2d | |||
451072276d | |||
e6bdcc9012 | |||
9e345752f2 | |||
23f4e14244 | |||
a53c2f5c44 | |||
ad64540bb6 | |||
697a752be0 | |||
3a1cfba251 | |||
7e3f632399 | |||
fa8dbe7988 | |||
bdeb5b2408 | |||
191bde9d1c | |||
8afdb6df6f | |||
a078da2715 | |||
1c287ee354 | |||
2ae8fd6e5b | |||
68deaec081 | |||
e737d990e9 | |||
01198b66ea | |||
943808562f | |||
ec994d51be | |||
9be7bafc53 | |||
cd387ca08b | |||
639db5d7eb | |||
e64e02342e | |||
51f91ede2c | |||
009da92285 | |||
780b3459af | |||
6acf1c67c1 | |||
27ad4fca43 | |||
dd0d7192eb | |||
8a42239f36 | |||
23c78997ac | |||
7cdfe618a2 | |||
7bfadca84d | |||
a7620ffd50 | |||
902cd9953e | |||
344c3613b8 | |||
32e1af0ffb | |||
1e1a03c9d8 | |||
4d3f7da48e | |||
2b26b9a707 | |||
82381457df | |||
d4b5120953 | |||
82152755c9 | |||
b251ef00c4 | |||
042fde971e | |||
c9e6ce0bca | |||
c4df53c195 | |||
3a06b3862d | |||
9fa5de1418 | |||
a9e7d2d314 | |||
b3f4712dd2 | |||
e5d009d87d | |||
70ca202889 | |||
b8aa8a0b4d | |||
1f755f535c | |||
f37e954e2f | |||
31bcd52c79 | |||
72f3a51657 | |||
3b4af20810 | |||
890c419133 | |||
f9680971af | |||
5df9332d4c | |||
4379d04b00 | |||
9d639097d3 | |||
cade2242bf | |||
1d7377f975 | |||
e7395f02ce | |||
c9e2a11745 | |||
9db38a130c | |||
25a69a2018 | |||
98c4a700a2 | |||
3defd3f486 | |||
b086190ab0 | |||
56008e80c7 | |||
0ac1a4f861 | |||
fb6d20e214 | |||
a9f6106c6e | |||
d1886fdd20 | |||
161b27e2f2 | |||
cb9119ac58 | |||
f31ce6b001 | |||
96e434213f | |||
b992f4d910 | |||
6b27beb355 | |||
4872e2e8c4 | |||
a37524c726 | |||
6cd3879bac | |||
c630f65317 | |||
76cfe30c0f | |||
a7872aaffd | |||
4635d85ff8 | |||
068e943fd3 | |||
c4e0b0272c | |||
ede5c9de51 | |||
524589717b | |||
88af032736 | |||
260a8800a4 | |||
37261c9a54 | |||
5073ec39bf | |||
3af9f55bf1 | |||
f7cbf73c44 | |||
1e9a87d6fa | |||
fe6fe39800 | |||
082776aca5 | |||
483b7917ac | |||
7f7353c505 | |||
adc73eb7ff | |||
c18be20899 | |||
198f596ab3 | |||
148d28eaca | |||
58a73e581f | |||
2d9b6b8394 | |||
aa9dd7ac6d | |||
47d5df1aa1 | |||
d644387091 | |||
db3a20c60b | |||
27a05ce6db | |||
7c0e37cc8e | |||
11d2df1fe8 | |||
a820929582 | |||
dcd1c97d37 | |||
6726217354 | |||
c1a825f8f2 | |||
563c73221e | |||
d35001049f | |||
652f3fb86b | |||
f877ba73a9 | |||
91078eec0f | |||
a57c982270 | |||
ed5d8faf5c | |||
f6857ac635 | |||
320b01d15c | |||
001ecc5961 | |||
4bdd240122 | |||
5fef69d697 | |||
8fc85ef4c1 | |||
85d88815f1 | |||
a0266c5e69 | |||
2ba0b1e7d3 | |||
3051d44b0d | |||
b8a310bb07 | |||
d11a5f862b | |||
08d250156c | |||
75378400e7 | |||
bb42861a92 | |||
dfecb99d07 | |||
1c66ac9117 | |||
55fb36863c | |||
034d887abd | |||
9f3f344daa | |||
ebe85a9ded | |||
06af995202 | |||
92e71ae2f4 | |||
f8505781a0 | |||
3b9b99a07e | |||
ab4ce41015 | |||
f613f0aace | |||
2b8d8fc4b7 | |||
ac32034910 | |||
9665d2d457 | |||
d73d68d9f4 | |||
50ba71c6fb | |||
38f1169061 | |||
5c90228320 | |||
03db194046 | |||
68382d3f61 | |||
4e99046874 | |||
64b320614b | |||
7b5f3e8e83 | |||
748841776f | |||
edfbb92a3f | |||
1a9a0e48b7 | |||
d27f1ded36 | |||
e5cd824c99 | |||
2542b7de12 | |||
f42a66e756 | |||
d301915273 | |||
fc43e47874 | |||
b0365a5a43 | |||
2a63a55359 | |||
0e9fd144f1 | |||
d81646087e | |||
7f1da4d1fc | |||
68f6be23a6 | |||
0b282b2664 | |||
665218f641 | |||
b64bce2936 | |||
042327840f | |||
3d468e32b9 | |||
16d2ec82b8 | |||
421e90cf70 | |||
8119ff9f83 | |||
253c7c8721 | |||
cb80def122 | |||
e669d0be82 | |||
495197c19d | |||
a5414c2c57 | |||
cbfb3919fc | |||
d789542d0f | |||
c6c2ec7784 | |||
4645bd84e8 | |||
10829b32ad | |||
e86904b11e | |||
82390340c9 | |||
163523d586 | |||
95d64df321 | |||
0b0290a871 | |||
5f588bb0f7 | |||
b99cc424e7 | |||
1dc0f5a240 | |||
43c4d4af38 | |||
db11a5f480 | |||
b51af7ca9a | |||
2cceb2f3e7 | |||
599a14b646 |
8
.gitignore
vendored
8
.gitignore
vendored
@ -221,10 +221,12 @@ DEPLOY
|
|||||||
global.min.css
|
global.min.css
|
||||||
global.min.js
|
global.min.js
|
||||||
bootstrap-custom.min.css
|
bootstrap-custom.min.css
|
||||||
|
bootstrap-custom.css
|
||||||
**/Master/static
|
**/Master/static
|
||||||
**/Master/dev_env
|
**/Master/dev_env
|
||||||
/WebfrontCore/Views/Plugins/*
|
/WebfrontCore/Views/Plugins/*
|
||||||
/WebfrontCore/wwwroot/**/dds
|
/WebfrontCore/wwwroot/**/dds
|
||||||
|
/WebfrontCore/wwwroot/images/radar/*
|
||||||
|
|
||||||
/DiscordWebhook/env
|
/DiscordWebhook/env
|
||||||
/DiscordWebhook/config.dev.json
|
/DiscordWebhook/config.dev.json
|
||||||
@ -234,3 +236,9 @@ launchSettings.json
|
|||||||
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
|
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
|
||||||
**/Master/env_master
|
**/Master/env_master
|
||||||
/GameLogServer/log_env
|
/GameLogServer/log_env
|
||||||
|
**/*.css
|
||||||
|
/Master/master/persistence
|
||||||
|
/WebfrontCore/wwwroot/fonts
|
||||||
|
/WebfrontCore/wwwroot/font
|
||||||
|
/Plugins/Tests/TestSourceFiles
|
||||||
|
/Tests/ApplicationTests/Files/GameEvents.json
|
||||||
|
@ -6,7 +6,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
|
|||||||
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
||||||
public interface IGameLogServer
|
public interface IGameLogServer
|
||||||
{
|
{
|
||||||
[Get("log/{path}")]
|
[Get("log/{path}/{key}")]
|
||||||
Task<LogInfo> Log([Path] string path);
|
Task<LogInfo> Log([Path] string path, [Path] string key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,5 +13,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
|
|||||||
public int Length { get; set; }
|
public int Length { get; set; }
|
||||||
[JsonProperty("data")]
|
[JsonProperty("data")]
|
||||||
public string Data { get; set; }
|
public string Data { get; set; }
|
||||||
|
[JsonProperty("next_key")]
|
||||||
|
public string NextKey { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,36 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RestEase;
|
using SharedLibraryCore.Helpers;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.API.Master
|
namespace IW4MAdmin.Application.API.Master
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the structure of the IW4MAdmin instance for the master API
|
||||||
|
/// </summary>
|
||||||
public class ApiInstance
|
public class ApiInstance
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique ID of the instance
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("id")]
|
[JsonProperty("id")]
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates how long the instance has been running
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("uptime")]
|
[JsonProperty("uptime")]
|
||||||
public int Uptime { get; set; }
|
public int Uptime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifices the version of the instance
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("version")]
|
[JsonProperty("version")]
|
||||||
public float Version { get; set; }
|
[JsonConverter(typeof(BuildNumberJsonConverter))]
|
||||||
|
public BuildNumber Version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of servers the instance is monitoring
|
||||||
|
/// </summary>
|
||||||
[JsonProperty("servers")]
|
[JsonProperty("servers")]
|
||||||
public List<ApiServer> Servers { get; set; }
|
public List<ApiServer> Servers { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RestEase;
|
using RestEase;
|
||||||
using SharedLibraryCore;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.API.Master
|
namespace IW4MAdmin.Application.API.Master
|
||||||
{
|
{
|
||||||
public class HeartbeatState
|
/// <summary>
|
||||||
{
|
/// Defines the heartbeat functionality for IW4MAdmin
|
||||||
public bool Connected { get; set; }
|
/// </summary>
|
||||||
public CancellationToken Token { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Heartbeat
|
public class Heartbeat
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends heartbeat to master server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mgr"></param>
|
||||||
|
/// <param name="firstHeartbeat"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
|
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
|
||||||
{
|
{
|
||||||
var api = Endpoint.Get();
|
var api = Endpoint.Get();
|
||||||
@ -35,7 +34,7 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
{
|
{
|
||||||
Id = mgr.GetApplicationSettings().Configuration().Id,
|
Id = mgr.GetApplicationSettings().Configuration().Id,
|
||||||
Uptime = (int)(DateTime.UtcNow - mgr.StartTime).TotalSeconds,
|
Uptime = (int)(DateTime.UtcNow - mgr.StartTime).TotalSeconds,
|
||||||
Version = (float)Program.Version,
|
Version = Program.Version,
|
||||||
Servers = mgr.Servers.Select(s =>
|
Servers = mgr.Servers.Select(s =>
|
||||||
new ApiServer()
|
new ApiServer()
|
||||||
{
|
{
|
||||||
@ -47,19 +46,26 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
Map = s.CurrentMap.Name,
|
Map = s.CurrentMap.Name,
|
||||||
MaxClientNum = s.MaxClients,
|
MaxClientNum = s.MaxClients,
|
||||||
Id = s.EndPoint,
|
Id = s.EndPoint,
|
||||||
Port = (short)s.GetPort(),
|
Port = (short)s.Port,
|
||||||
IPAddress = s.IP
|
IPAddress = s.IP
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Response<ResultMessage> response = null;
|
||||||
|
|
||||||
if (firstHeartbeat)
|
if (firstHeartbeat)
|
||||||
{
|
{
|
||||||
var message = await api.AddInstance(instance);
|
response = await api.AddInstance(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var message = await api.UpdateInstance(instance.Id, instance);
|
response = await api.UpdateInstance(instance.Id, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
mgr.Logger.WriteWarning($"Response code from master is {response.ResponseMessage.StatusCode}, message is {response.StringContent}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RestEase;
|
using RestEase;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.API.Master
|
namespace IW4MAdmin.Application.API.Master
|
||||||
{
|
{
|
||||||
@ -22,9 +23,12 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
public class VersionInfo
|
public class VersionInfo
|
||||||
{
|
{
|
||||||
[JsonProperty("current-version-stable")]
|
[JsonProperty("current-version-stable")]
|
||||||
public float CurrentVersionStable { get; set; }
|
[JsonConverter(typeof(BuildNumberJsonConverter))]
|
||||||
|
public BuildNumber CurrentVersionStable { get; set; }
|
||||||
|
|
||||||
[JsonProperty("current-version-prerelease")]
|
[JsonProperty("current-version-prerelease")]
|
||||||
public float CurrentVersionPrerelease { get; set; }
|
[JsonConverter(typeof(BuildNumberJsonConverter))]
|
||||||
|
public BuildNumber CurrentVersionPrerelease { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResultMessage
|
public class ResultMessage
|
||||||
@ -38,11 +42,14 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
|
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
|
||||||
#else
|
#else
|
||||||
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
|
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
|
||||||
#endif
|
#endif
|
||||||
public static IMasterApi Get() => api;
|
public static IMasterApi Get() => api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the capabilities of the master API
|
||||||
|
/// </summary>
|
||||||
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
||||||
public interface IMasterApi
|
public interface IMasterApi
|
||||||
{
|
{
|
||||||
@ -53,13 +60,15 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
Task<TokenId> Authenticate([Body] AuthenticationId Id);
|
Task<TokenId> Authenticate([Body] AuthenticationId Id);
|
||||||
|
|
||||||
[Post("instance/")]
|
[Post("instance/")]
|
||||||
Task<ResultMessage> AddInstance([Body] ApiInstance instance);
|
[AllowAnyStatusCode]
|
||||||
|
Task<Response<ResultMessage>> AddInstance([Body] ApiInstance instance);
|
||||||
|
|
||||||
[Put("instance/{id}")]
|
[Put("instance/{id}")]
|
||||||
Task<ResultMessage> UpdateInstance([Path] string id, [Body] ApiInstance instance);
|
[AllowAnyStatusCode]
|
||||||
|
Task<Response<ResultMessage>> UpdateInstance([Path] string id, [Body] ApiInstance instance);
|
||||||
|
|
||||||
[Get("version")]
|
[Get("version/{apiVersion}")]
|
||||||
Task<VersionInfo> GetVersion();
|
Task<VersionInfo> GetVersion([Path] int apiVersion);
|
||||||
|
|
||||||
[Get("localization")]
|
[Get("localization")]
|
||||||
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();
|
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
|
|
||||||
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||||
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
||||||
<Version>2.2.6.5</Version>
|
<Version>2.3.2.0</Version>
|
||||||
<Authors>RaidMax</Authors>
|
<Authors>RaidMax</Authors>
|
||||||
<Company>Forever None</Company>
|
<Company>Forever None</Company>
|
||||||
<Product>IW4MAdmin</Product>
|
<Product>IW4MAdmin</Product>
|
||||||
@ -21,28 +20,39 @@
|
|||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
<Win32Resource />
|
<Win32Resource />
|
||||||
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
||||||
|
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="RestEase" Version="1.4.7" />
|
<PackageReference Include="Jint" Version="3.0.0-beta-1632" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
|
||||||
|
<PackageReference Include="RestEase" Version="1.4.10" />
|
||||||
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<TieredCompilation>true</TieredCompilation>
|
<TieredCompilation>true</TieredCompilation>
|
||||||
<AssemblyVersion>2.2.6.5</AssemblyVersion>
|
<LangVersion>7.1</LangVersion>
|
||||||
<FileVersion>2.2.6.5</FileVersion>
|
<StartupObject></StartupObject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
|
||||||
|
<ErrorReport>none</ErrorReport>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
|
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||||
<Private>true</Private>
|
<Private>true</Private>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj">
|
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj" />
|
||||||
<Private>true</Private>
|
|
||||||
<CopyLocalSatelliteAssemblies>true</CopyLocalSatelliteAssemblies>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -51,22 +61,18 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||||
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
|
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
|
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
|
||||||
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
|
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
|
||||||
</GetAssemblyIdentity>
|
</GetAssemblyIdentity>
|
||||||
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %(CurrentAssembly.Version)" />
|
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %25(CurrentAssembly.Version)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PostPublish" AfterTargets="Publish">
|
<Target Name="PostPublish" AfterTargets="Publish">
|
||||||
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
|
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -5,92 +5,102 @@ using IW4MAdmin.Application.RconParsers;
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Configuration.Validation;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Dtos;
|
using SharedLibraryCore.Dtos;
|
||||||
using SharedLibraryCore.Events;
|
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using SharedLibraryCore.Services;
|
using SharedLibraryCore.Services;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static SharedLibraryCore.GameEvent;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
public class ApplicationManager : IManager
|
public class ApplicationManager : IManager
|
||||||
{
|
{
|
||||||
private List<Server> _servers;
|
private readonly ConcurrentBag<Server> _servers;
|
||||||
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
|
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
|
||||||
public Dictionary<int, EFClient> PrivilegedClients { get; set; }
|
|
||||||
public ILogger Logger => GetLogger(0);
|
public ILogger Logger => GetLogger(0);
|
||||||
public bool Running { get; private set; }
|
public bool Running { get; private set; }
|
||||||
public bool IsInitialized { get; private set; }
|
public bool IsInitialized { get; private set; }
|
||||||
// define what the delagate function looks like
|
|
||||||
public delegate void OnServerEventEventHandler(object sender, GameEventArgs e);
|
|
||||||
// expose the event handler so we can execute the events
|
|
||||||
public OnServerEventEventHandler OnServerEvent { get; set; }
|
|
||||||
public DateTime StartTime { get; private set; }
|
public DateTime StartTime { get; private set; }
|
||||||
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
|
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
|
||||||
|
|
||||||
public IList<IRConParser> AdditionalRConParsers { get; }
|
public IList<IRConParser> AdditionalRConParsers { get; }
|
||||||
public IList<IEventParser> AdditionalEventParsers { get; }
|
public IList<IEventParser> AdditionalEventParsers { get; }
|
||||||
public ITokenAuthentication TokenAuthenticator => Authenticator;
|
public ITokenAuthentication TokenAuthenticator { get; }
|
||||||
public ITokenAuthentication Authenticator => _authenticator;
|
public CancellationToken CancellationToken => _tokenSource.Token;
|
||||||
public string ExternalIPAddress { get; private set; }
|
public string ExternalIPAddress { get; private set; }
|
||||||
|
public bool IsRestartRequested { get; private set; }
|
||||||
static ApplicationManager Instance;
|
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
|
||||||
readonly List<AsyncStatus> TaskStatuses;
|
private readonly List<IManagerCommand> _commands;
|
||||||
List<Command> Commands;
|
private readonly ILogger _logger;
|
||||||
readonly List<MessageToken> MessageTokens;
|
private readonly List<MessageToken> MessageTokens;
|
||||||
ClientService ClientSvc;
|
private readonly ClientService ClientSvc;
|
||||||
readonly AliasService AliasSvc;
|
readonly AliasService AliasSvc;
|
||||||
readonly PenaltyService PenaltySvc;
|
readonly PenaltyService PenaltySvc;
|
||||||
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||||
GameEventHandler Handler;
|
GameEventHandler Handler;
|
||||||
ManualResetEventSlim OnQuit;
|
|
||||||
readonly IPageList PageList;
|
readonly IPageList PageList;
|
||||||
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
|
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
|
||||||
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
|
|
||||||
readonly ITokenAuthentication _authenticator;
|
|
||||||
private readonly MetaService _metaService;
|
private readonly MetaService _metaService;
|
||||||
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||||
|
private readonly CancellationTokenSource _tokenSource;
|
||||||
|
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
|
||||||
|
private readonly ITranslationLookup _translationLookup;
|
||||||
|
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
|
||||||
|
private readonly IGameServerInstanceFactory _serverInstanceFactory;
|
||||||
|
private readonly IParserRegexFactory _parserRegexFactory;
|
||||||
|
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
|
||||||
|
|
||||||
private ApplicationManager()
|
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
|
||||||
|
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
|
||||||
|
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
|
||||||
|
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents)
|
||||||
{
|
{
|
||||||
_servers = new List<Server>();
|
MiddlewareActionHandler = actionHandler;
|
||||||
Commands = new List<Command>();
|
_servers = new ConcurrentBag<Server>();
|
||||||
TaskStatuses = new List<AsyncStatus>();
|
|
||||||
MessageTokens = new List<MessageToken>();
|
MessageTokens = new List<MessageToken>();
|
||||||
ClientSvc = new ClientService();
|
ClientSvc = new ClientService();
|
||||||
AliasSvc = new AliasService();
|
AliasSvc = new AliasService();
|
||||||
PenaltySvc = new PenaltyService();
|
PenaltySvc = new PenaltyService();
|
||||||
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
ConfigHandler = appConfigHandler;
|
||||||
StartTime = DateTime.UtcNow;
|
StartTime = DateTime.UtcNow;
|
||||||
OnQuit = new ManualResetEventSlim();
|
|
||||||
PageList = new PageList();
|
PageList = new PageList();
|
||||||
AdditionalEventParsers = new List<IEventParser>();
|
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger) };
|
||||||
AdditionalRConParsers = new List<IRConParser>();
|
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
|
||||||
OnServerEvent += OnGameEvent;
|
TokenAuthenticator = new TokenAuthentication();
|
||||||
OnServerEvent += EventApi.OnGameEvent;
|
_logger = logger;
|
||||||
_authenticator = new TokenAuthentication();
|
|
||||||
_metaService = new MetaService();
|
_metaService = new MetaService();
|
||||||
|
_tokenSource = new CancellationTokenSource();
|
||||||
|
_loggers.Add(0, logger);
|
||||||
|
_commands = commands.ToList();
|
||||||
|
_translationLookup = translationLookup;
|
||||||
|
_commandConfiguration = commandConfiguration;
|
||||||
|
_serverInstanceFactory = serverInstanceFactory;
|
||||||
|
_parserRegexFactory = parserRegexFactory;
|
||||||
|
_customParserEvents = customParserEvents;
|
||||||
|
Plugins = plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnGameEvent(object sender, GameEventArgs args)
|
public IEnumerable<IPlugin> Plugins { get; }
|
||||||
|
|
||||||
|
public async Task ExecuteEvent(GameEvent newEvent)
|
||||||
{
|
{
|
||||||
#if DEBUG == true
|
#if DEBUG == true
|
||||||
Logger.WriteDebug($"Entering event process for {args.Event.Id}");
|
Logger.WriteDebug($"Entering event process for {newEvent.Id}");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var newEvent = args.Event;
|
|
||||||
|
|
||||||
// the event has failed already
|
// the event has failed already
|
||||||
if (newEvent.Failed)
|
if (newEvent.Failed)
|
||||||
{
|
{
|
||||||
@ -103,44 +113,56 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
// save the event info to the database
|
// save the event info to the database
|
||||||
var changeHistorySvc = new ChangeHistoryService();
|
var changeHistorySvc = new ChangeHistoryService();
|
||||||
await changeHistorySvc.Add(args.Event);
|
await changeHistorySvc.Add(newEvent);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
|
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
|
||||||
|
}
|
||||||
|
|
||||||
// this happens if a plugin requires login
|
// this happens if a plugin requires login
|
||||||
catch (AuthorizationException ex)
|
catch (AuthorizationException ex)
|
||||||
{
|
{
|
||||||
newEvent.FailReason = GameEvent.EventFailReason.Permission;
|
newEvent.FailReason = EventFailReason.Permission;
|
||||||
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
|
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (NetworkException ex)
|
catch (NetworkException ex)
|
||||||
{
|
{
|
||||||
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
newEvent.FailReason = EventFailReason.Exception;
|
||||||
Logger.WriteError(ex.Message);
|
Logger.WriteError(ex.Message);
|
||||||
Logger.WriteDebug(ex.GetExceptionInfo());
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (ServerException ex)
|
catch (ServerException ex)
|
||||||
{
|
{
|
||||||
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
newEvent.FailReason = EventFailReason.Exception;
|
||||||
Logger.WriteWarning(ex.Message);
|
Logger.WriteWarning(ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
newEvent.FailReason = EventFailReason.Exception;
|
||||||
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
|
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
|
||||||
Logger.WriteDebug(ex.GetExceptionInfo());
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
skip:
|
skip:
|
||||||
|
|
||||||
// tell anyone waiting for the output that we're done
|
// tell anyone waiting for the output that we're done
|
||||||
newEvent.OnProcessed.Set();
|
newEvent.Complete();
|
||||||
|
#if DEBUG == true
|
||||||
|
Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<Server> GetServers()
|
public IList<Server> GetServers()
|
||||||
@ -148,22 +170,17 @@ namespace IW4MAdmin.Application
|
|||||||
return Servers;
|
return Servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<Command> GetCommands()
|
public IList<IManagerCommand> GetCommands()
|
||||||
{
|
{
|
||||||
return Commands;
|
return _commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ApplicationManager GetInstance()
|
public async Task UpdateServerStates()
|
||||||
{
|
|
||||||
return Instance ?? (Instance = new ApplicationManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateServerStates(CancellationToken token)
|
|
||||||
{
|
{
|
||||||
// store the server hash code and task for it
|
// store the server hash code and task for it
|
||||||
var runningUpdateTasks = new Dictionary<long, Task>();
|
var runningUpdateTasks = new Dictionary<long, Task>();
|
||||||
|
|
||||||
while (Running)
|
while (!_tokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// select the server ids that have completed the update task
|
// select the server ids that have completed the update task
|
||||||
var serverTasksToRemove = runningUpdateTasks
|
var serverTasksToRemove = runningUpdateTasks
|
||||||
@ -194,10 +211,11 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await server.ProcessUpdatesAsync(token);
|
await server.ProcessUpdatesAsync(_tokenSource.Token);
|
||||||
|
|
||||||
if (server.Throttled)
|
if (server.Throttled)
|
||||||
{
|
{
|
||||||
await Task.Delay((int)_throttleTimeout.TotalMilliseconds);
|
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,10 +239,17 @@ namespace IW4MAdmin.Application
|
|||||||
#endif
|
#endif
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(ConfigHandler.Configuration().RConPollRate, token);
|
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
|
||||||
|
}
|
||||||
|
// if a cancellation is received, we want to return immediately after shutting down
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
|
||||||
|
{
|
||||||
|
await server.ProcessUpdatesAsync(_tokenSource.Token);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// if a cancellation is received, we want to return immediately
|
|
||||||
catch { break; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,18 +259,37 @@ namespace IW4MAdmin.Application
|
|||||||
ExternalIPAddress = await Utilities.GetExternalIP();
|
ExternalIPAddress = await Utilities.GetExternalIP();
|
||||||
|
|
||||||
#region PLUGINS
|
#region PLUGINS
|
||||||
SharedLibraryCore.Plugins.PluginImporter.Load(this);
|
foreach (var plugin in Plugins)
|
||||||
|
|
||||||
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Plugin.OnLoadAsync(this);
|
if (plugin is ScriptPlugin scriptPlugin)
|
||||||
|
{
|
||||||
|
await scriptPlugin.Initialize(this);
|
||||||
|
scriptPlugin.Watcher.Changed += async (sender, e) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await scriptPlugin.Initialize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}");
|
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
|
||||||
|
Logger.WriteDebug(ex.Message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await plugin.OnLoadAsync(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
|
||||||
Logger.WriteDebug(ex.GetExceptionInfo());
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,7 +314,7 @@ namespace IW4MAdmin.Application
|
|||||||
if (newConfig.Servers == null)
|
if (newConfig.Servers == null)
|
||||||
{
|
{
|
||||||
ConfigHandler.Set(newConfig);
|
ConfigHandler.Set(newConfig);
|
||||||
newConfig.Servers = new List<ServerConfiguration>();
|
newConfig.Servers = new ServerConfiguration[1];
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@ -285,8 +329,8 @@ namespace IW4MAdmin.Application
|
|||||||
serverConfig.AddEventParser(parser);
|
serverConfig.AddEventParser(parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig.Servers.Add((ServerConfiguration)serverConfig.Generate());
|
newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
|
||||||
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
|
} while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
|
||||||
|
|
||||||
config = newConfig;
|
config = newConfig;
|
||||||
await ConfigHandler.Save();
|
await ConfigHandler.Save();
|
||||||
@ -307,6 +351,18 @@ namespace IW4MAdmin.Application
|
|||||||
await ConfigHandler.Save();
|
await ConfigHandler.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var validator = new ApplicationConfigurationValidator();
|
||||||
|
var validationResult = validator.Validate(config);
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
{
|
||||||
|
throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
|
||||||
|
{
|
||||||
|
Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(),
|
||||||
|
ConfigurationFileName = ConfigHandler.FileName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var serverConfig in config.Servers)
|
foreach (var serverConfig in config.Servers)
|
||||||
{
|
{
|
||||||
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
|
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
|
||||||
@ -329,7 +385,7 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.Servers.Count == 0)
|
if (config.Servers.Length == 0)
|
||||||
{
|
{
|
||||||
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
|
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
|
||||||
}
|
}
|
||||||
@ -345,59 +401,43 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
await new ContextSeed(db).Seed();
|
await new ContextSeed(db).Seed();
|
||||||
}
|
}
|
||||||
|
|
||||||
PrivilegedClients = (await ClientSvc.GetPrivilegedClients()).ToDictionary(_client => _client.ClientId);
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region COMMANDS
|
#region COMMANDS
|
||||||
if (ClientSvc.GetOwners().Result.Count == 0)
|
if (ClientSvc.GetOwners().Result.Count > 0)
|
||||||
{
|
{
|
||||||
Commands.Add(new COwner());
|
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands.Add(new CQuit());
|
List<IManagerCommand> commandsToAddToConfig = new List<IManagerCommand>();
|
||||||
Commands.Add(new CKick());
|
var cmdConfig = _commandConfiguration.Configuration();
|
||||||
Commands.Add(new CSay());
|
|
||||||
Commands.Add(new CTempBan());
|
|
||||||
Commands.Add(new CBan());
|
|
||||||
Commands.Add(new CWhoAmI());
|
|
||||||
Commands.Add(new CList());
|
|
||||||
Commands.Add(new CHelp());
|
|
||||||
Commands.Add(new CFastRestart());
|
|
||||||
Commands.Add(new CMapRotate());
|
|
||||||
Commands.Add(new CSetLevel());
|
|
||||||
Commands.Add(new CUsage());
|
|
||||||
Commands.Add(new CUptime());
|
|
||||||
Commands.Add(new CWarn());
|
|
||||||
Commands.Add(new CWarnClear());
|
|
||||||
Commands.Add(new CUnban());
|
|
||||||
Commands.Add(new CListAdmins());
|
|
||||||
Commands.Add(new CLoadMap());
|
|
||||||
Commands.Add(new CFindPlayer());
|
|
||||||
Commands.Add(new CListRules());
|
|
||||||
Commands.Add(new CPrivateMessage());
|
|
||||||
Commands.Add(new CFlag());
|
|
||||||
Commands.Add(new CUnflag());
|
|
||||||
Commands.Add(new CReport());
|
|
||||||
Commands.Add(new CListReports());
|
|
||||||
Commands.Add(new CListBanInfo());
|
|
||||||
Commands.Add(new CListAlias());
|
|
||||||
Commands.Add(new CExecuteRCON());
|
|
||||||
Commands.Add(new CPlugins());
|
|
||||||
Commands.Add(new CIP());
|
|
||||||
Commands.Add(new CMask());
|
|
||||||
Commands.Add(new CPruneAdmins());
|
|
||||||
Commands.Add(new CKillServer());
|
|
||||||
Commands.Add(new CSetPassword());
|
|
||||||
Commands.Add(new CPing());
|
|
||||||
Commands.Add(new CSetGravatar());
|
|
||||||
Commands.Add(new CNextMap());
|
|
||||||
Commands.Add(new RequestTokenCommand());
|
|
||||||
|
|
||||||
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
if (cmdConfig == null)
|
||||||
{
|
{
|
||||||
Commands.Add(C);
|
cmdConfig = new CommandConfiguration();
|
||||||
|
commandsToAddToConfig.AddRange(_commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.GetType().Name));
|
||||||
|
commandsToAddToConfig.AddRange(unsavedCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cmd in commandsToAddToConfig)
|
||||||
|
{
|
||||||
|
cmdConfig.Commands.Add(cmd.GetType().Name,
|
||||||
|
new CommandProperties()
|
||||||
|
{
|
||||||
|
Name = cmd.Name,
|
||||||
|
Alias = cmd.Alias,
|
||||||
|
MinimumPermission = cmd.Permission,
|
||||||
|
AllowImpersonation = cmd.AllowImpersonation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_commandConfiguration.Set(cmdConfig);
|
||||||
|
await _commandConfiguration.Save();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region META
|
#region META
|
||||||
@ -441,6 +481,12 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
var client = await GetClientService().Get(clientId);
|
var client = await GetClientService().Get(clientId);
|
||||||
|
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
_logger.WriteWarning($"No client found with id {clientId} when generating profile meta");
|
||||||
|
return metaList;
|
||||||
|
}
|
||||||
|
|
||||||
metaList.Add(new ProfileMeta()
|
metaList.Add(new ProfileMeta()
|
||||||
{
|
{
|
||||||
Id = client.ClientId,
|
Id = client.ClientId,
|
||||||
@ -522,26 +568,37 @@ namespace IW4MAdmin.Application
|
|||||||
MetaService.AddRuntimeMeta(getPenaltyMeta);
|
MetaService.AddRuntimeMeta(getPenaltyMeta);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region INIT
|
#region CUSTOM_EVENTS
|
||||||
int failedServers = 0;
|
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
||||||
|
{
|
||||||
|
foreach (var parser in AdditionalEventParsers)
|
||||||
|
{
|
||||||
|
parser.RegisterCustomEvent(customEvent.Item1, customEvent.Item2, customEvent.Item3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
await InitializeServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeServers()
|
||||||
|
{
|
||||||
|
var config = ConfigHandler.Configuration();
|
||||||
int successServers = 0;
|
int successServers = 0;
|
||||||
Exception lastException = null;
|
Exception lastException = null;
|
||||||
|
|
||||||
async Task Init(ServerConfiguration Conf)
|
async Task Init(ServerConfiguration Conf)
|
||||||
{
|
{
|
||||||
// setup the event handler after the class is initialized
|
// setup the event handler after the class is initialized
|
||||||
|
|
||||||
Handler = new GameEventHandler(this);
|
Handler = new GameEventHandler(this);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ServerInstance = new IW4MServer(this, Conf);
|
// todo: this might not always be an IW4MServer
|
||||||
|
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
|
||||||
await ServerInstance.Initialize();
|
await ServerInstance.Initialize();
|
||||||
|
|
||||||
lock (_servers)
|
|
||||||
{
|
|
||||||
_servers.Add(ServerInstance);
|
_servers.Add(ServerInstance);
|
||||||
}
|
|
||||||
|
|
||||||
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
|
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
|
||||||
// add the start event for this server
|
// add the start event for this server
|
||||||
@ -563,48 +620,46 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
if (e.GetType() == typeof(DvarException))
|
if (e.GetType() == typeof(DvarException))
|
||||||
{
|
{
|
||||||
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt((e as DvarException).Data["dvar_name"])} ({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})");
|
Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (e.GetType() == typeof(NetworkException))
|
|
||||||
{
|
|
||||||
Logger.WriteDebug(e.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
failedServers++;
|
|
||||||
lastException = e;
|
lastException = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
||||||
|
|
||||||
if (successServers - failedServers <= 0)
|
if (successServers == 0)
|
||||||
|
{
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successServers != config.Servers.Length)
|
||||||
{
|
{
|
||||||
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
|
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
|
||||||
{
|
{
|
||||||
throw lastException;
|
throw lastException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendHeartbeat(object state)
|
private async Task SendHeartbeat()
|
||||||
{
|
{
|
||||||
var heartbeatState = (HeartbeatState)state;
|
bool connected = false;
|
||||||
|
|
||||||
while (Running)
|
while (!_tokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (!heartbeatState.Connected)
|
if (!connected)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Heartbeat.Send(this, true);
|
await Heartbeat.Send(this, true);
|
||||||
heartbeatState.Connected = true;
|
connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
heartbeatState.Connected = false;
|
connected = false;
|
||||||
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
|
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -630,7 +685,7 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
heartbeatState.Connected = false;
|
connected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -640,9 +695,10 @@ namespace IW4MAdmin.Application
|
|||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
heartbeatState.Connected = false;
|
connected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
@ -652,54 +708,45 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(30000, heartbeatState.Token);
|
await Task.Delay(30000, _tokenSource.Token);
|
||||||
}
|
}
|
||||||
catch { break; }
|
catch { break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
var tokenSource = new CancellationTokenSource();
|
await Task.WhenAll(new[]
|
||||||
// this needs to be run seperately from the main thread
|
|
||||||
_ = Task.Run(() => SendHeartbeat(new HeartbeatState() { Token = tokenSource.Token }));
|
|
||||||
_ = Task.Run(() => UpdateServerStates(tokenSource.Token));
|
|
||||||
|
|
||||||
while (Running)
|
|
||||||
{
|
{
|
||||||
OnQuit.Wait();
|
SendHeartbeat(),
|
||||||
tokenSource.Cancel();
|
UpdateServerStates()
|
||||||
OnQuit.Reset();
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
|
_tokenSource.Cancel();
|
||||||
Running = false;
|
Running = false;
|
||||||
OnQuit.Set();
|
}
|
||||||
|
|
||||||
|
public void Restart()
|
||||||
|
{
|
||||||
|
IsRestartRequested = true;
|
||||||
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger GetLogger(long serverId)
|
public ILogger GetLogger(long serverId)
|
||||||
{
|
{
|
||||||
if (Loggers.ContainsKey(serverId))
|
if (_loggers.ContainsKey(serverId))
|
||||||
{
|
{
|
||||||
return Loggers[serverId];
|
return _loggers[serverId];
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger newLogger;
|
var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
|
||||||
|
|
||||||
if (serverId == 0)
|
_loggers.Add(serverId, newLogger);
|
||||||
{
|
|
||||||
newLogger = new Logger("IW4MAdmin-Manager");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Loggers.Add(serverId, newLogger);
|
|
||||||
return newLogger;
|
return newLogger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,7 +758,8 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
public IList<EFClient> GetActiveClients()
|
public IList<EFClient> GetActiveClients()
|
||||||
{
|
{
|
||||||
return _servers.SelectMany(s => s.Clients).Where(p => p != null).ToList();
|
// we're adding another to list here so we don't get a collection modified exception..
|
||||||
|
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientService GetClientService()
|
public ClientService GetClientService()
|
||||||
@ -734,44 +782,41 @@ namespace IW4MAdmin.Application
|
|||||||
return ConfigHandler;
|
return ConfigHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<int, EFClient> GetPrivilegedClients()
|
|
||||||
{
|
|
||||||
return PrivilegedClients;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShutdownRequested()
|
|
||||||
{
|
|
||||||
return !Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEventHandler GetEventHandler()
|
public IEventHandler GetEventHandler()
|
||||||
{
|
{
|
||||||
return Handler;
|
return Handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetHasEvent()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<Assembly> GetPluginAssemblies()
|
|
||||||
{
|
|
||||||
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies.Union(SharedLibraryCore.Plugins.PluginImporter.Assemblies).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPageList GetPageList()
|
public IPageList GetPageList()
|
||||||
{
|
{
|
||||||
return PageList;
|
return PageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRConParser GenerateDynamicRConParser()
|
public IRConParser GenerateDynamicRConParser(string name)
|
||||||
{
|
{
|
||||||
return new DynamicRConParser();
|
return new DynamicRConParser(_parserRegexFactory)
|
||||||
|
{
|
||||||
|
Name = name
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEventParser GenerateDynamicEventParser()
|
public IEventParser GenerateDynamicEventParser(string name)
|
||||||
{
|
{
|
||||||
return new DynamicEventParser();
|
return new DynamicEventParser(_parserRegexFactory, _logger)
|
||||||
|
{
|
||||||
|
Name = name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IList<T>> ExecuteSharedDatabaseOperation<T>(string operationName)
|
||||||
|
{
|
||||||
|
var result = await _operationLookup[operationName];
|
||||||
|
return (IList<T>)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSharedDatabaseOperation(Task<IList> operation, string operationName)
|
||||||
|
{
|
||||||
|
_operationLookup.Add(operationName, operation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,3 @@ xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Pl
|
|||||||
echo Copying script plugins for publish
|
echo Copying script plugins for publish
|
||||||
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
|
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
|
||||||
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
|
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
|
||||||
|
|
||||||
echo Copying GSC files for publish
|
|
||||||
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
|
|
||||||
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"
|
|
||||||
|
|
||||||
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
|
|
||||||
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"
|
|
@ -1,115 +1,41 @@
|
|||||||
set SolutionDir=%1
|
set PublishDir=%1
|
||||||
set ProjectDir=%2
|
set SourceDir=%2
|
||||||
set TargetDir=%3
|
|
||||||
set CurrentConfiguration=%4
|
|
||||||
SET COPYCMD=/Y
|
SET COPYCMD=/Y
|
||||||
|
|
||||||
echo Deleting extra language files
|
echo deleting extra runtime files
|
||||||
|
if exist "%PublishDir%\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-arm'
|
||||||
|
if exist "%PublishDir%\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-arm64'
|
||||||
|
if exist "%PublishDir%\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-armel'
|
||||||
|
if exist "%PublishDir%\runtimes\osx" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\osx'
|
||||||
|
if exist "%PublishDir%\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\osx-x64'
|
||||||
|
if exist "%PublishDir%\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\win-arm'
|
||||||
|
if exist "%PublishDir%\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\win-arm64'
|
||||||
|
if exist "%PublishDir%\runtimes\alpine-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\alpine-x64'
|
||||||
|
if exist "%PublishDir%\runtimes\linux-musl-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-musl-x64'
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\Windows\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\en-US'
|
echo deleting misc files
|
||||||
if exist "%SolutionDir%Publish\Windows\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\de'
|
if exist "%PublishDir%\web.config" del "%PublishDir%\web.config"
|
||||||
if exist "%SolutionDir%Publish\Windows\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\es'
|
if exist "%PublishDir%\libman.json" del "%PublishDir%\libman.json"
|
||||||
if exist "%SolutionDir%Publish\Windows\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\fr'
|
del "%PublishDir%\*.exe"
|
||||||
if exist "%SolutionDir%Publish\Windows\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\it'
|
del "%PublishDir%\*.pdb"
|
||||||
if exist "%SolutionDir%Publish\Windows\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ja'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ko'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ru'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hans'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hant'
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\en-US'
|
echo setting up default folders
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\de'
|
if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\es'
|
move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\fr'
|
if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\it'
|
move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ja'
|
move "%PublishDir%\*.json" "%PublishDir%\Lib\"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ko'
|
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ru'
|
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hans'
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hant'
|
|
||||||
|
|
||||||
echo Deleting extra runtime files
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm64'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-armel'
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\osx'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\osx-x64'
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm'
|
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm64'
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm'
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64'
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel'
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx'
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64'
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm'
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64'
|
|
||||||
|
|
||||||
echo Deleting misc files
|
|
||||||
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
|
|
||||||
del "%SolutionDir%Publish\Windows\*pdb"
|
|
||||||
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
|
|
||||||
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
|
|
||||||
|
|
||||||
echo setting up library folders
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Prerelease" (
|
|
||||||
echo PR-Config
|
|
||||||
if not exist "%SolutionDir%Publish\WindowsPrerelease\Configuration" md "%SolutionDir%Publish\WindowsPrerelease\Configuration"
|
|
||||||
move "%SolutionDir%Publish\WindowsPrerelease\DefaultSettings.json" "%SolutionDir%Publish\WindowsPrerelease\Configuration\"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Release" (
|
|
||||||
echo R-Config
|
|
||||||
if not exist "%SolutionDir%Publish\Windows\Configuration" md "%SolutionDir%Publish\Windows\Configuration"
|
|
||||||
if exist "%SolutionDir%Publish\Windows\DefaultSettings.json" move "%SolutionDir%Publish\Windows\DefaultSettings.json" "%SolutionDir%Publish\Windows\Configuration\DefaultSettings.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Prerelease" (
|
|
||||||
echo PR-LIB
|
|
||||||
if not exist "%SolutionDir%Publish\WindowsPrerelease\Lib\" md "%SolutionDir%Publish\WindowsPrerelease\Lib\"
|
|
||||||
move "%SolutionDir%Publish\WindowsPrerelease\*.dll" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
|
|
||||||
move "%SolutionDir%Publish\WindowsPrerelease\*.json" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Release" (
|
|
||||||
echo R-LIB
|
|
||||||
if not exist "%SolutionDir%Publish\Windows\Lib\" md "%SolutionDir%Publish\Windows\Lib\"
|
|
||||||
move "%SolutionDir%Publish\Windows\*.dll" "%SolutionDir%Publish\Windows\Lib\"
|
|
||||||
move "%SolutionDir%Publish\Windows\*.json" "%SolutionDir%Publish\Windows\Lib\"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Prerelease" (
|
|
||||||
echo PR-RT
|
|
||||||
move "%SolutionDir%Publish\WindowsPrerelease\runtimes" "%SolutionDir%Publish\WindowsPrerelease\Lib\runtimes"
|
|
||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\refs" move "%SolutionDir%Publish\WindowsPrerelease\refs" "%SolutionDir%Publish\WindowsPrerelease\Lib\refs"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Release" (
|
|
||||||
echo R-RT
|
|
||||||
move "%SolutionDir%Publish\Windows\runtimes" "%SolutionDir%Publish\Windows\Lib\runtimes"
|
|
||||||
if exist "%SolutionDir%Publish\Windows\refs" move "%SolutionDir%Publish\Windows\refs" "%SolutionDir%Publish\Windows\Lib\refs"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Prerelease" (
|
|
||||||
echo PR-LOC
|
|
||||||
if not exist "%SolutionDir%Publish\WindowsPrerelease\Localization" md "%SolutionDir%Publish\WindowsPrerelease\Localization"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%CurrentConfiguration%" == "Release" (
|
|
||||||
echo R-LOC
|
|
||||||
if not exist "%SolutionDir%Publish\Windows\Localization" md "%SolutionDir%Publish\Windows\Localization"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo making start scripts
|
echo making start scripts
|
||||||
@(echo @echo off && echo @title IW4MAdmin && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
|
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd"
|
||||||
@(echo @echo off && echo @title IW4MAdmin && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
|
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh"
|
||||||
|
|
||||||
@(echo #!/bin/bash && echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
|
echo moving front-end library dependencies
|
||||||
@(echo #!/bin/bash && echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
|
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
|
||||||
|
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\"
|
||||||
|
if exist "%PublishDir%\wwwroot\lib" rd /s /q "%PublishDir%\wwwroot\lib"
|
||||||
|
|
||||||
|
echo setting permissions...
|
||||||
|
cacls "%PublishDir%" /t /e /p Everyone:F
|
@ -16,7 +16,7 @@
|
|||||||
"Keep grenade launcher use to a minimum",
|
"Keep grenade launcher use to a minimum",
|
||||||
"Balance teams at ALL times"
|
"Balance teams at ALL times"
|
||||||
],
|
],
|
||||||
"DisallowedClientNames": ["Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER"],
|
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
|
||||||
"QuickMessages": [
|
"QuickMessages": [
|
||||||
{
|
{
|
||||||
"Game": "IW4",
|
"Game": "IW4",
|
||||||
@ -517,6 +517,10 @@
|
|||||||
"Alias": "Hanoi",
|
"Alias": "Hanoi",
|
||||||
"Name": "mp_hanoi"
|
"Name": "mp_hanoi"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Havana",
|
||||||
|
"Name": "mp_cairo"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Alias": "Hazard",
|
"Alias": "Hazard",
|
||||||
"Name": "mp_golfcourse"
|
"Name": "mp_golfcourse"
|
||||||
|
@ -2,17 +2,23 @@
|
|||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using static SharedLibraryCore.Server;
|
using static SharedLibraryCore.Server;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
class BaseEventParser : IEventParser
|
public class BaseEventParser : IEventParser
|
||||||
{
|
{
|
||||||
public BaseEventParser()
|
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger)
|
||||||
{
|
{
|
||||||
Configuration = new DynamicEventParserConfiguration()
|
_customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
|
||||||
{
|
{
|
||||||
GameDirectory = "main",
|
GameDirectory = "main",
|
||||||
};
|
};
|
||||||
@ -36,7 +42,7 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
|
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
|
||||||
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
|
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
|
||||||
|
|
||||||
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
||||||
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
|
||||||
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
|
||||||
@ -51,7 +57,7 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
|
||||||
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
|
||||||
|
|
||||||
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
||||||
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
|
||||||
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
|
||||||
@ -65,6 +71,8 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
|
||||||
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
|
||||||
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
|
||||||
|
|
||||||
|
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEventParserConfiguration Configuration { get; set; }
|
public IEventParserConfiguration Configuration { get; set; }
|
||||||
@ -75,50 +83,56 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
|
|
||||||
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
|
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
|
||||||
|
|
||||||
public virtual GameEvent GetEvent(Server server, string logLine)
|
public string Name { get; set; } = "Call of Duty";
|
||||||
|
|
||||||
|
public virtual GameEvent GenerateGameEvent(string logLine)
|
||||||
{
|
{
|
||||||
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
|
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
|
||||||
|
int gameTime = 0;
|
||||||
|
|
||||||
|
if (timeMatch.Success)
|
||||||
|
{
|
||||||
|
gameTime = timeMatch
|
||||||
|
.Values
|
||||||
|
.Skip(2)
|
||||||
|
// this converts the timestamp into seconds passed
|
||||||
|
.Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
|
||||||
|
.Sum();
|
||||||
|
// we want to strip the time from the log line
|
||||||
|
logLine = logLine.Substring(timeMatch.Values.First().Length);
|
||||||
|
}
|
||||||
|
|
||||||
string[] lineSplit = logLine.Split(';');
|
string[] lineSplit = logLine.Split(';');
|
||||||
string eventType = lineSplit[0];
|
string eventType = lineSplit[0];
|
||||||
|
|
||||||
if (eventType == "JoinTeam")
|
|
||||||
{
|
|
||||||
var origin = server.GetClientsAsList()
|
|
||||||
.FirstOrDefault(c => c.NetworkId == lineSplit[1].ConvertLong());
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.JoinTeam,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = origin,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "say" || eventType == "sayteam")
|
if (eventType == "say" || eventType == "sayteam")
|
||||||
{
|
{
|
||||||
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
|
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
if (matchResult.Success)
|
if (matchResult.Success)
|
||||||
{
|
{
|
||||||
string message = matchResult
|
string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
||||||
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
|
||||||
.ToString()
|
.ToString()
|
||||||
.Replace("\x15", "")
|
.Replace("\x15", "")
|
||||||
.Trim();
|
.Trim();
|
||||||
|
|
||||||
var origin = server.GetClientsAsList()
|
if (message.Length > 0)
|
||||||
.First(c => c.NetworkId == matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong());
|
{
|
||||||
|
long originId = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
|
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
|
||||||
|
// todo: these need to defined outside of here
|
||||||
if (message[0] == '!' || message[0] == '@')
|
if (message[0] == '!' || message[0] == '@')
|
||||||
{
|
{
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Command,
|
Type = GameEvent.EventType.Command,
|
||||||
Data = message,
|
Data = message,
|
||||||
Origin = origin,
|
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
|
||||||
Owner = server,
|
Message = message,
|
||||||
Message = message
|
Extra = logLine,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,165 +140,112 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Say,
|
Type = GameEvent.EventType.Say,
|
||||||
Data = message,
|
Data = message,
|
||||||
Origin = origin,
|
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
|
||||||
Owner = server,
|
Message = message,
|
||||||
Message = message
|
Extra = logLine,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (eventType == "K")
|
if (eventType == "K")
|
||||||
{
|
{
|
||||||
if (!server.CustomCallback)
|
var match = Configuration.Kill.PatternMatcher.Match(logLine);
|
||||||
{
|
|
||||||
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
|
|
||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
string originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString();
|
long originId = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
|
||||||
string targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString();
|
long targetId = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
|
||||||
|
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
var origin = !string.IsNullOrEmpty(originId) ? server.GetClientsAsList()
|
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
|
||||||
.First(c => c.NetworkId == originId.ConvertLong()) :
|
|
||||||
Utilities.IW4MAdminClient(server);
|
|
||||||
|
|
||||||
var target = !string.IsNullOrEmpty(targetId) ? server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == targetId.ConvertLong()) :
|
|
||||||
Utilities.IW4MAdminClient(server);
|
|
||||||
|
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Kill,
|
Type = GameEvent.EventType.Kill,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Origin = origin,
|
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
|
||||||
Target = target,
|
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
|
||||||
Owner = server
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "ScriptKill")
|
|
||||||
{
|
|
||||||
long originId = lineSplit[1].ConvertLong();
|
|
||||||
long targetId = lineSplit[2].ConvertLong();
|
|
||||||
|
|
||||||
var origin = originId == long.MinValue ? Utilities.IW4MAdminClient(server) :
|
|
||||||
server.GetClientsAsList().First(c => c.NetworkId == originId);
|
|
||||||
var target = targetId == long.MinValue ? Utilities.IW4MAdminClient(server) :
|
|
||||||
server.GetClientsAsList().FirstOrDefault(c => c.NetworkId == targetId) ?? Utilities.IW4MAdminClient(server);
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.ScriptKill,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = origin,
|
|
||||||
Target = target,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "ScriptDamage")
|
|
||||||
{
|
|
||||||
long originId = lineSplit[1].ConvertLong();
|
|
||||||
long targetId = lineSplit[2].ConvertLong();
|
|
||||||
|
|
||||||
var origin = originId == long.MinValue ? Utilities.IW4MAdminClient(server) :
|
|
||||||
server.GetClientsAsList().First(c => c.NetworkId == originId);
|
|
||||||
var target = targetId == long.MinValue ? Utilities.IW4MAdminClient(server) :
|
|
||||||
server.GetClientsAsList().FirstOrDefault(c => c.NetworkId == targetId) ?? Utilities.IW4MAdminClient(server);
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.ScriptDamage,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = origin,
|
|
||||||
Target = target,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// damage
|
|
||||||
if (eventType == "D")
|
if (eventType == "D")
|
||||||
{
|
{
|
||||||
if (!server.CustomCallback)
|
var match = Configuration.Damage.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
{
|
{
|
||||||
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
|
long originId = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
|
||||||
|
long targetId = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
|
||||||
if (regexMatch.Success)
|
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
{
|
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
|
||||||
string originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
|
|
||||||
string targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
|
|
||||||
|
|
||||||
var origin = !string.IsNullOrEmpty(originId) ? server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == originId.ConvertLong()) :
|
|
||||||
Utilities.IW4MAdminClient(server);
|
|
||||||
|
|
||||||
var target = !string.IsNullOrEmpty(targetId) ? server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == targetId.ConvertLong()) :
|
|
||||||
Utilities.IW4MAdminClient(server);
|
|
||||||
|
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Damage,
|
Type = GameEvent.EventType.Damage,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Origin = origin,
|
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
|
||||||
Target = target,
|
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
|
||||||
Owner = server
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// join
|
|
||||||
if (eventType == "J")
|
if (eventType == "J")
|
||||||
{
|
{
|
||||||
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
|
var match = Configuration.Join.PatternMatcher.Match(logLine);
|
||||||
if (regexMatch.Success)
|
|
||||||
{
|
|
||||||
bool isBot = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().Contains("bot");
|
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.PreConnect,
|
Type = GameEvent.EventType.PreConnect,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Owner = server,
|
|
||||||
Origin = new EFClient()
|
Origin = new EFClient()
|
||||||
{
|
{
|
||||||
CurrentAlias = new EFAlias()
|
CurrentAlias = new EFAlias()
|
||||||
{
|
{
|
||||||
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors(),
|
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
|
||||||
},
|
},
|
||||||
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
|
NetworkId = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
|
||||||
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
||||||
State = EFClient.ClientState.Connecting,
|
State = EFClient.ClientState.Connecting,
|
||||||
CurrentServer = server,
|
},
|
||||||
IsBot = isBot
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
}
|
IsBlocking = true,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType == "Q")
|
if (eventType == "Q")
|
||||||
{
|
{
|
||||||
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
|
var match = Configuration.Quit.PatternMatcher.Match(logLine);
|
||||||
if (regexMatch.Success)
|
|
||||||
|
if (match.Success)
|
||||||
{
|
{
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.PreDisconnect,
|
Type = GameEvent.EventType.PreDisconnect,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Owner = server,
|
|
||||||
Origin = new EFClient()
|
Origin = new EFClient()
|
||||||
{
|
{
|
||||||
CurrentAlias = new EFAlias()
|
CurrentAlias = new EFAlias()
|
||||||
{
|
{
|
||||||
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors()
|
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
|
||||||
},
|
},
|
||||||
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
|
NetworkId = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
|
||||||
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
||||||
State = EFClient.ClientState.Disconnecting
|
State = EFClient.ClientState.Disconnecting
|
||||||
}
|
},
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
IsBlocking = true,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,10 +255,11 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.MapEnd,
|
Type = GameEvent.EventType.MapEnd,
|
||||||
Data = lineSplit[0],
|
Data = logLine,
|
||||||
Origin = Utilities.IW4MAdminClient(server),
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
Target = Utilities.IW4MAdminClient(server),
|
Target = Utilities.IW4MAdminClient(),
|
||||||
Owner = server
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,21 +270,71 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.MapChange,
|
Type = GameEvent.EventType.MapChange,
|
||||||
Data = lineSplit[0],
|
Data = logLine,
|
||||||
Origin = Utilities.IW4MAdminClient(server),
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
Target = Utilities.IW4MAdminClient(server),
|
Target = Utilities.IW4MAdminClient(),
|
||||||
Owner = server,
|
Extra = dump.DictionaryFromKeyValue(),
|
||||||
Extra = dump.DictionaryFromKeyValue()
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_customEventRegistrations.ContainsKey(eventType))
|
||||||
|
{
|
||||||
|
var eventModifier = _customEventRegistrations[eventType];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return eventModifier.Item2(logLine, Configuration, new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Other,
|
||||||
|
Data = logLine,
|
||||||
|
Subtype = eventModifier.Item1,
|
||||||
|
GameTime = gameTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Unknown,
|
Type = GameEvent.EventType.Unknown,
|
||||||
Origin = Utilities.IW4MAdminClient(server),
|
Data = logLine,
|
||||||
Target = Utilities.IW4MAdminClient(server),
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
Owner = server
|
Target = Utilities.IW4MAdminClient(),
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
GameTime = gameTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(eventSubtype))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Event subtype cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(eventTriggerValue))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Event trigger value cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventModifier == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Event modifier must be specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_customEventRegistrations.ContainsKey(eventTriggerValue))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Event trigger value '{eventTriggerValue}' is already registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
_customEventRegistrations.Add(eventTriggerValue, (eventSubtype, eventModifier));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using static SharedLibraryCore.Server;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
@ -11,5 +8,8 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
sealed internal class DynamicEventParser : BaseEventParser
|
sealed internal class DynamicEventParser : BaseEventParser
|
||||||
{
|
{
|
||||||
|
public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger) : base(parserRegexFactory, logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
@ -9,11 +10,24 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
|
sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
|
||||||
{
|
{
|
||||||
public string GameDirectory { get; set; }
|
public string GameDirectory { get; set; }
|
||||||
public ParserRegex Say { get; set; } = new ParserRegex();
|
public ParserRegex Say { get; set; }
|
||||||
public ParserRegex Join { get; set; } = new ParserRegex();
|
public ParserRegex Join { get; set; }
|
||||||
public ParserRegex Quit { get; set; } = new ParserRegex();
|
public ParserRegex Quit { get; set; }
|
||||||
public ParserRegex Kill { get; set; } = new ParserRegex();
|
public ParserRegex Kill { get; set; }
|
||||||
public ParserRegex Damage { get; set; } = new ParserRegex();
|
public ParserRegex Damage { get; set; }
|
||||||
public ParserRegex Action { get; set; } = new ParserRegex();
|
public ParserRegex Action { get; set; }
|
||||||
|
public ParserRegex Time { get; set; }
|
||||||
|
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
|
||||||
|
|
||||||
|
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
|
||||||
|
{
|
||||||
|
Say = parserRegexFactory.CreateParserRegex();
|
||||||
|
Join = parserRegexFactory.CreateParserRegex();
|
||||||
|
Quit = parserRegexFactory.CreateParserRegex();
|
||||||
|
Kill = parserRegexFactory.CreateParserRegex();
|
||||||
|
Damage = parserRegexFactory.CreateParserRegex();
|
||||||
|
Action = parserRegexFactory.CreateParserRegex();
|
||||||
|
Time = parserRegexFactory.CreateParserRegex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
Application/EventParsers/ParserPatternMatcher.cs
Normal file
35
Application/EventParsers/ParserPatternMatcher.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of the IParserPatternMatcher for windows (really it's the only implementation)
|
||||||
|
/// </summary>
|
||||||
|
public class ParserPatternMatcher : IParserPatternMatcher
|
||||||
|
{
|
||||||
|
private Regex regex;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Compile(string pattern)
|
||||||
|
{
|
||||||
|
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IMatchResult Match(string input)
|
||||||
|
{
|
||||||
|
var match = regex.Match(input);
|
||||||
|
|
||||||
|
return new ParserMatchResult()
|
||||||
|
{
|
||||||
|
Success = match.Success,
|
||||||
|
Values = (match.Groups as IEnumerable<object>)?
|
||||||
|
.Select(_item => _item.ToString()).ToArray() ?? new string[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
Application/Factories/ConfigurationHandlerFactory.cs
Normal file
23
Application/Factories/ConfigurationHandlerFactory.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Factories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IConfigurationHandlerFactory
|
||||||
|
/// provides base functionality to create configuration handlers
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationHandlerFactory : IConfigurationHandlerFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// creates a base configuration handler
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">base configuration type</typeparam>
|
||||||
|
/// <param name="name">name of the config file</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration
|
||||||
|
{
|
||||||
|
return new BaseConfigurationHandler<T>(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Application/Factories/DatabaseContextFactory.cs
Normal file
21
Application/Factories/DatabaseContextFactory.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using SharedLibraryCore.Database;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Factories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of the IDatabaseContextFactory interface
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseContextFactory : IDatabaseContextFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// creates a new database context
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enableTracking">indicates if entity tracking should be enabled</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DatabaseContext CreateContext(bool? enableTracking = true)
|
||||||
|
{
|
||||||
|
return enableTracking.HasValue ? new DatabaseContext(disableTracking: !enableTracking.Value) : new DatabaseContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
Application/Factories/GameServerInstanceFactory.cs
Normal file
38
Application/Factories/GameServerInstanceFactory.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Factories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IGameServerInstanceFactory
|
||||||
|
/// </summary>
|
||||||
|
internal class GameServerInstanceFactory : IGameServerInstanceFactory
|
||||||
|
{
|
||||||
|
private readonly ITranslationLookup _translationLookup;
|
||||||
|
private readonly IRConConnectionFactory _rconConnectionFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// base constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="translationLookup"></param>
|
||||||
|
/// <param name="rconConnectionFactory"></param>
|
||||||
|
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory)
|
||||||
|
{
|
||||||
|
_translationLookup = translationLookup;
|
||||||
|
_rconConnectionFactory = rconConnectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// creates an IW4MServer instance
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">server configuration</param>
|
||||||
|
/// <param name="manager">application manager</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Server CreateServer(ServerConfiguration config, IManager manager)
|
||||||
|
{
|
||||||
|
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
Application/Factories/ParserRegexFactory.cs
Normal file
26
Application/Factories/ParserRegexFactory.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Factories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of the IParserRegexFactory
|
||||||
|
/// </summary>
|
||||||
|
public class ParserRegexFactory : IParserRegexFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ParserRegexFactory(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ParserRegex CreateParserRegex()
|
||||||
|
{
|
||||||
|
return new ParserRegex(_serviceProvider.GetService<IParserPatternMatcher>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Application/Factories/RConConnectionFactory.cs
Normal file
36
Application/Factories/RConConnectionFactory.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using IW4MAdmin.Application.RCon;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Factories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IRConConnectionFactory
|
||||||
|
/// </summary>
|
||||||
|
internal class RConConnectionFactory : IRConConnectionFactory
|
||||||
|
{
|
||||||
|
private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252");
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
public RConConnectionFactory(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// creates a new rcon connection instance
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">ip address of the server</param>
|
||||||
|
/// <param name="port">port of the server</param>
|
||||||
|
/// <param name="password">rcon password of the server</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IRConConnection CreateConnection(string ipAddress, int port, string password)
|
||||||
|
{
|
||||||
|
return new RConConnection(ipAddress, port, password, _logger, gameEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,67 @@
|
|||||||
using SharedLibraryCore;
|
using IW4MAdmin.Application.Misc;
|
||||||
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Events;
|
using SharedLibraryCore.Events;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
class GameEventHandler : IEventHandler
|
class GameEventHandler : IEventHandler
|
||||||
{
|
{
|
||||||
readonly IManager Manager;
|
readonly ApplicationManager Manager;
|
||||||
|
private readonly EventProfiler _profiler;
|
||||||
|
private static readonly GameEvent.EventType[] overrideEvents = new[]
|
||||||
|
{
|
||||||
|
GameEvent.EventType.Connect,
|
||||||
|
GameEvent.EventType.Disconnect,
|
||||||
|
GameEvent.EventType.Quit,
|
||||||
|
GameEvent.EventType.Stop
|
||||||
|
};
|
||||||
|
|
||||||
public GameEventHandler(IManager mgr)
|
public GameEventHandler(IManager mgr)
|
||||||
{
|
{
|
||||||
Manager = mgr;
|
Manager = (ApplicationManager)mgr;
|
||||||
|
_profiler = new EventProfiler(mgr.GetLogger(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task GameEventHandler_GameEventAdded(object sender, GameEventArgs args)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
var start = DateTime.Now;
|
||||||
|
#endif
|
||||||
|
EventApi.OnGameEvent(sender, args);
|
||||||
|
return Manager.ExecuteEvent(args.Event);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
_profiler.Profile(start, DateTime.Now, args.Event);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddEvent(GameEvent gameEvent)
|
public void AddEvent(GameEvent gameEvent)
|
||||||
{
|
{
|
||||||
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
|
#if DEBUG
|
||||||
|
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
|
||||||
|
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
|
||||||
|
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
|
||||||
|
|
||||||
|
#endif
|
||||||
|
if (Manager.Running || overrideEvents.Contains(gameEvent.Type))
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
|
||||||
|
#endif
|
||||||
|
//GameEventAdded?.Invoke(this, new GameEventArgs(null, false, gameEvent));
|
||||||
|
Task.Run(() => GameEventHandler_GameEventAdded(this, new GameEventArgs(null, false, gameEvent)));
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gameEvent.Owner.Logger.WriteDebug($"Skipping event as we're shutting down {gameEvent.Id}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.IO
|
namespace IW4MAdmin.Application.IO
|
||||||
{
|
{
|
||||||
class GameLogEventDetection
|
public class GameLogEventDetection
|
||||||
{
|
{
|
||||||
Server Server;
|
private long previousFileSize;
|
||||||
long PreviousFileSize;
|
private readonly Server _server;
|
||||||
IGameLogReader Reader;
|
private readonly IGameLogReader _reader;
|
||||||
readonly string GameLogFile;
|
private readonly bool _ignoreBots;
|
||||||
|
|
||||||
class EventState
|
class EventState
|
||||||
{
|
{
|
||||||
@ -19,18 +19,20 @@ namespace IW4MAdmin.Application.IO
|
|||||||
public string ServerId { get; set; }
|
public string ServerId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri)
|
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri, IGameLogReader reader = null)
|
||||||
{
|
{
|
||||||
GameLogFile = gameLogPath;
|
_reader = gameLogServerUri != null
|
||||||
Reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : Reader = new GameLogReader(gameLogPath, server.EventParser);
|
? reader ?? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser)
|
||||||
Server = server;
|
: reader ?? new GameLogReader(gameLogPath, server.EventParser);
|
||||||
|
_server = server;
|
||||||
|
_ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PollForChanges()
|
public async Task PollForChanges()
|
||||||
{
|
{
|
||||||
while (!Server.Manager.ShutdownRequested())
|
while (!_server.Manager.CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (Server.IsInitialized)
|
if (_server.IsInitialized)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -39,39 +41,84 @@ namespace IW4MAdmin.Application.IO
|
|||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Server.Logger.WriteWarning($"Failed to update log event for {Server.EndPoint}");
|
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
|
||||||
Server.Logger.WriteDebug($"Exception: {e.Message}");
|
_server.Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
Server.Logger.WriteDebug($"StackTrace: {e.StackTrace}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Thread.Sleep(Reader.UpdateInterval);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateLogEvents()
|
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_server.Logger.WriteDebug("Stopped polling for changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLogEvents()
|
||||||
{
|
{
|
||||||
long fileSize = Reader.Length;
|
long fileSize = _reader.Length;
|
||||||
|
|
||||||
if (PreviousFileSize == 0)
|
if (previousFileSize == 0)
|
||||||
PreviousFileSize = fileSize;
|
{
|
||||||
|
previousFileSize = fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
long fileDiff = fileSize - PreviousFileSize;
|
long fileDiff = fileSize - previousFileSize;
|
||||||
|
|
||||||
// this makes the http log get pulled
|
// this makes the http log get pulled
|
||||||
if (fileDiff < 1 && fileSize != -1)
|
if (fileDiff < 1 && fileSize != -1)
|
||||||
return;
|
|
||||||
|
|
||||||
PreviousFileSize = fileSize;
|
|
||||||
|
|
||||||
var events = await Reader.ReadEventsFromLog(Server, fileDiff, 0);
|
|
||||||
|
|
||||||
foreach (var ev in events)
|
|
||||||
{
|
{
|
||||||
Server.Manager.GetEventHandler().AddEvent(ev);
|
previousFileSize = fileSize;
|
||||||
await ev.WaitAsync();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PreviousFileSize = fileSize;
|
var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize);
|
||||||
|
|
||||||
|
foreach (var gameEvent in events)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
_server.Logger.WriteVerbose(gameEvent.Data);
|
||||||
|
#endif
|
||||||
|
gameEvent.Owner = _server;
|
||||||
|
|
||||||
|
// we don't want to add the event if ignoreBots is on and the event comes from a bot
|
||||||
|
if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
|
||||||
|
{
|
||||||
|
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1)
|
||||||
|
{
|
||||||
|
gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
|
||||||
|
{
|
||||||
|
gameEvent.Target = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameEvent.Origin != null)
|
||||||
|
{
|
||||||
|
gameEvent.Origin.CurrentServer = _server;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameEvent.Target != null)
|
||||||
|
{
|
||||||
|
gameEvent.Target.CurrentServer = _server;
|
||||||
|
}
|
||||||
|
|
||||||
|
_server.Manager.GetEventHandler().AddEvent(gameEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
if (!_ignoreBots)
|
||||||
|
{
|
||||||
|
_server.Logger.WriteWarning("Could not find client in client list when parsing event line");
|
||||||
|
_server.Logger.WriteDebug(gameEvent.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFileSize = fileSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using SharedLibraryCore.Interfaces;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -10,49 +11,62 @@ namespace IW4MAdmin.Application.IO
|
|||||||
{
|
{
|
||||||
class GameLogReader : IGameLogReader
|
class GameLogReader : IGameLogReader
|
||||||
{
|
{
|
||||||
IEventParser Parser;
|
private readonly IEventParser _parser;
|
||||||
readonly string LogFile;
|
private readonly string _logFile;
|
||||||
|
|
||||||
public long Length => new FileInfo(LogFile).Length;
|
public long Length => new FileInfo(_logFile).Length;
|
||||||
|
|
||||||
public int UpdateInterval => 300;
|
public int UpdateInterval => 300;
|
||||||
|
|
||||||
public GameLogReader(string logFile, IEventParser parser)
|
public GameLogReader(string logFile, IEventParser parser)
|
||||||
{
|
{
|
||||||
LogFile = logFile;
|
_logFile = logFile;
|
||||||
Parser = parser;
|
_parser = parser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
||||||
{
|
{
|
||||||
// allocate the bytes for the new log lines
|
// allocate the bytes for the new log lines
|
||||||
List<string> logLines = new List<string>();
|
List<string> logLines = new List<string>();
|
||||||
|
|
||||||
// open the file as a stream
|
// open the file as a stream
|
||||||
using (var rd = new StreamReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
|
using (FileStream fs = new FileStream(_logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
{
|
{
|
||||||
// todo: max async
|
byte[] buff = new byte[fileSizeDiff];
|
||||||
// take the old start position and go back the number of new characters
|
fs.Seek(startPosition, SeekOrigin.Begin);
|
||||||
rd.BaseStream.Seek(-fileSizeDiff, SeekOrigin.End);
|
await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken);
|
||||||
|
var stringBuilder = new StringBuilder();
|
||||||
|
char[] charBuff = Utilities.EncodingType.GetChars(buff);
|
||||||
|
|
||||||
string newLine;
|
foreach (char c in charBuff)
|
||||||
while (!string.IsNullOrEmpty(newLine = await rd.ReadLineAsync()))
|
|
||||||
{
|
{
|
||||||
logLines.Add(newLine);
|
if (c == '\n')
|
||||||
|
{
|
||||||
|
logLines.Add(stringBuilder.ToString());
|
||||||
|
stringBuilder = new StringBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (c != '\r')
|
||||||
|
{
|
||||||
|
stringBuilder.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringBuilder.Length > 0)
|
||||||
|
{
|
||||||
|
logLines.Add(stringBuilder.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<GameEvent> events = new List<GameEvent>();
|
List<GameEvent> events = new List<GameEvent>();
|
||||||
|
|
||||||
// parse each line
|
// parse each line
|
||||||
foreach (string eventLine in logLines)
|
foreach (string eventLine in logLines.Where(_line => _line.Length > 0))
|
||||||
{
|
|
||||||
if (eventLine.Length > 0)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// todo: catch elsewhere
|
var gameEvent = _parser.GenerateGameEvent(eventLine);
|
||||||
events.Add(Parser.GetEvent(server, eventLine));
|
events.Add(gameEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -62,7 +76,6 @@ namespace IW4MAdmin.Application.IO
|
|||||||
server.Logger.WriteDebug(eventLine);
|
server.Logger.WriteDebug(eventLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using SharedLibraryCore;
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using static SharedLibraryCore.Utilities;
|
using static SharedLibraryCore.Utilities;
|
||||||
@ -15,53 +16,54 @@ namespace IW4MAdmin.Application.IO
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
class GameLogReaderHttp : IGameLogReader
|
class GameLogReaderHttp : IGameLogReader
|
||||||
{
|
{
|
||||||
readonly IEventParser Parser;
|
private readonly IEventParser _eventParser;
|
||||||
readonly IGameLogServer Api;
|
private readonly IGameLogServer _logServerApi;
|
||||||
readonly string logPath;
|
readonly string logPath;
|
||||||
|
private string lastKey = "next";
|
||||||
|
|
||||||
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
|
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
|
||||||
{
|
{
|
||||||
this.logPath = logPath.ToBase64UrlSafeString(); ;
|
this.logPath = logPath.ToBase64UrlSafeString();
|
||||||
Parser = parser;
|
_eventParser = parser;
|
||||||
Api = RestClient.For<IGameLogServer>(gameLogServerUri);
|
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long Length => -1;
|
public long Length => -1;
|
||||||
|
|
||||||
public int UpdateInterval => 350;
|
public int UpdateInterval => 500;
|
||||||
|
|
||||||
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
||||||
{
|
{
|
||||||
#if DEBUG == true
|
|
||||||
server.Logger.WriteDebug($"Begin reading from http log");
|
|
||||||
#endif
|
|
||||||
var events = new List<GameEvent>();
|
var events = new List<GameEvent>();
|
||||||
string b64Path = logPath;
|
string b64Path = logPath;
|
||||||
var response = await Api.Log(b64Path);
|
var response = await _logServerApi.Log(b64Path, lastKey);
|
||||||
|
lastKey = response.NextKey;
|
||||||
|
|
||||||
if (!response.Success)
|
if (!response.Success && string.IsNullOrEmpty(lastKey))
|
||||||
{
|
{
|
||||||
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
|
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse each line
|
else if (!string.IsNullOrWhiteSpace(response.Data))
|
||||||
foreach (string eventLine in response.Data.Split(Environment.NewLine))
|
|
||||||
{
|
{
|
||||||
if (eventLine.Length > 0)
|
// parse each line
|
||||||
|
var lines = response.Data
|
||||||
|
.Split(Environment.NewLine)
|
||||||
|
.Where(_line => _line.Length > 0);
|
||||||
|
|
||||||
|
foreach (string eventLine in lines)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var e = Parser.GetEvent(server, eventLine);
|
// this trim end should hopefully fix the nasty runaway regex
|
||||||
#if DEBUG == true
|
var gameEvent = _eventParser.GenerateGameEvent(eventLine.TrimEnd('\r'));
|
||||||
server.Logger.WriteDebug($"Parsed event with id {e.Id} from http");
|
events.Add(gameEvent);
|
||||||
#endif
|
|
||||||
events.Add(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
server.Logger.WriteWarning("Could not properly parse event line");
|
server.Logger.WriteError("Could not properly parse event line from http");
|
||||||
server.Logger.WriteDebug(e.Message);
|
server.Logger.WriteDebug(e.Message);
|
||||||
server.Logger.WriteDebug(eventLine);
|
server.Logger.WriteDebug(eventLine);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,30 +1,29 @@
|
|||||||
using IW4MAdmin.Application.API.Master;
|
using IW4MAdmin.Application.API.Master;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Localization
|
namespace IW4MAdmin.Application.Localization
|
||||||
{
|
{
|
||||||
public class Configure
|
public class Configure
|
||||||
{
|
{
|
||||||
public static void Initialize(string customLocale = null)
|
public static ITranslationLookup Initialize(bool useLocalTranslation, string customLocale = null)
|
||||||
{
|
{
|
||||||
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
|
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
|
||||||
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
|
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
|
||||||
|
|
||||||
if (!Program.ServerManager.GetApplicationSettings()?.Configuration()?.UseLocalTranslations ?? false)
|
if (!useLocalTranslation)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var api = Endpoint.Get();
|
var api = Endpoint.Get();
|
||||||
var localization = api.GetLocalization(currentLocale).Result;
|
var localization = api.GetLocalization(currentLocale).Result;
|
||||||
Utilities.CurrentLocalization = localization;
|
Utilities.CurrentLocalization = localization;
|
||||||
return;
|
return localization.LocalizationIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@ -73,6 +72,8 @@ namespace IW4MAdmin.Application.Localization
|
|||||||
{
|
{
|
||||||
LocalizationName = currentLocale,
|
LocalizationName = currentLocale,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
using IW4MAdmin.Application.Migration;
|
using IW4MAdmin.Application.EventParsers;
|
||||||
|
using IW4MAdmin.Application.Factories;
|
||||||
|
using IW4MAdmin.Application.Helpers;
|
||||||
|
using IW4MAdmin.Application.Migration;
|
||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Localization;
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.Repositories;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -11,61 +20,166 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
static public double Version { get; private set; }
|
public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString());
|
||||||
static public ApplicationManager ServerManager;
|
public static ApplicationManager ServerManager;
|
||||||
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
|
private static Task ApplicationTask;
|
||||||
|
private static readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
|
||||||
|
private static ServiceProvider serviceProvider;
|
||||||
|
|
||||||
public static void Main(string[] args)
|
/// <summary>
|
||||||
|
/// entrypoint of the application
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task Main()
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
|
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
|
||||||
|
|
||||||
Console.OutputEncoding = Encoding.UTF8;
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
|
|
||||||
Version = Utilities.GetVersionAsDouble();
|
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
||||||
|
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
Console.WriteLine(" IW4M ADMIN");
|
Console.WriteLine(" IW4MAdmin");
|
||||||
Console.WriteLine(" by RaidMax ");
|
Console.WriteLine(" by RaidMax ");
|
||||||
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
|
|
||||||
Index loc = null;
|
await LaunchAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// event callback executed when the control + c combination is detected
|
||||||
|
/// gracefully stops the server manager and waits for all tasks to finish
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
/// <param name="e"></param>
|
||||||
|
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
|
||||||
|
{
|
||||||
|
ServerManager?.Stop();
|
||||||
|
await ApplicationTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// task that initializes application and starts the application monitoring and runtime tasks
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static async Task LaunchAsync()
|
||||||
|
{
|
||||||
|
restart:
|
||||||
|
ITranslationLookup translationLookup = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ServerManager = ApplicationManager.GetInstance();
|
// do any needed housekeeping file/folder migrations
|
||||||
var configuration = ServerManager.GetApplicationSettings().Configuration();
|
ConfigurationMigration.MoveConfigFolder10518(null);
|
||||||
|
ConfigurationMigration.CheckDirectories();
|
||||||
|
|
||||||
if (configuration != null)
|
var services = ConfigureServices();
|
||||||
|
serviceProvider = services.BuildServiceProvider();
|
||||||
|
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
|
||||||
|
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
|
||||||
|
|
||||||
|
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
|
||||||
|
|
||||||
|
await CheckVersion(translationLookup);
|
||||||
|
await ServerManager.Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Localization.Configure.Initialize(configuration.EnableCustomLocale ? (configuration.CustomLocale ?? "windows-1252") : "windows-1252");
|
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
|
||||||
|
string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
|
||||||
|
|
||||||
|
Console.WriteLine(failMessage);
|
||||||
|
|
||||||
|
while (e.InnerException != null)
|
||||||
|
{
|
||||||
|
e = e.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e is ConfigurationException configException)
|
||||||
|
{
|
||||||
|
if (translationLookup != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string error in configException.Errors)
|
||||||
|
{
|
||||||
|
Console.WriteLine(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Localization.Configure.Initialize();
|
Console.WriteLine(e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
loc = Utilities.CurrentLocalization.LocalizationIndex;
|
Console.WriteLine(exitMessage);
|
||||||
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
await Console.In.ReadAsync(new char[1], 0, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CheckDirectories();
|
try
|
||||||
// do any needed migrations
|
{
|
||||||
// todo: move out
|
ApplicationTask = RunApplicationTasksAsync();
|
||||||
ConfigurationMigration.MoveConfigFolder10518(null);
|
await ApplicationTask;
|
||||||
|
}
|
||||||
|
|
||||||
ServerManager.Logger.WriteInfo($"Version is {Version}");
|
catch { }
|
||||||
|
|
||||||
|
if (ServerManager.IsRestartRequested)
|
||||||
|
{
|
||||||
|
goto restart;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceProvider.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// runs the core application tasks
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static async Task RunApplicationTasksAsync()
|
||||||
|
{
|
||||||
|
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
|
||||||
|
WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) :
|
||||||
|
Task.CompletedTask;
|
||||||
|
|
||||||
|
// we want to run this one on a manual thread instead of letting the thread pool handle it,
|
||||||
|
// because we can't exit early from waiting on console input, and it prevents us from restarting
|
||||||
|
var inputThread = new Thread(async () => await ReadConsoleInput());
|
||||||
|
inputThread.Start();
|
||||||
|
|
||||||
|
var tasks = new[]
|
||||||
|
{
|
||||||
|
ServerManager.Start(),
|
||||||
|
webfrontTask,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// checks for latest version of the application
|
||||||
|
/// notifies user if an update is available
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static async Task CheckVersion(ITranslationLookup translationLookup)
|
||||||
|
{
|
||||||
var api = API.Master.Endpoint.Get();
|
var api = API.Master.Endpoint.Get();
|
||||||
|
var loc = translationLookup;
|
||||||
|
|
||||||
var version = new API.Master.VersionInfo()
|
var version = new API.Master.VersionInfo()
|
||||||
{
|
{
|
||||||
CurrentVersionStable = 99.99f
|
CurrentVersionStable = _fallbackVersion
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
version = api.GetVersion().Result;
|
version = await api.GetVersion(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -79,7 +193,7 @@ namespace IW4MAdmin.Application
|
|||||||
ServerManager.Logger.WriteDebug(e.Message);
|
ServerManager.Logger.WriteDebug(e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version.CurrentVersionStable == 99.99f)
|
if (version.CurrentVersionStable == _fallbackVersion)
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.Red;
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
|
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
|
||||||
@ -90,16 +204,16 @@ namespace IW4MAdmin.Application
|
|||||||
else if (version.CurrentVersionStable > Version)
|
else if (version.CurrentVersionStable > Version)
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||||
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
|
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString()}]");
|
||||||
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}]"));
|
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}]"));
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
else if (version.CurrentVersionPrerelease > Version)
|
else if (version.CurrentVersionPrerelease > Version)
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||||
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
|
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString()}-pr]");
|
||||||
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}-pr]"));
|
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}-pr]"));
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -109,97 +223,120 @@ namespace IW4MAdmin.Application
|
|||||||
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
|
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ServerManager.Init().Wait();
|
/// <summary>
|
||||||
|
/// reads input from the console and executes entered commands on the default server
|
||||||
var consoleTask = Task.Run(async () =>
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static async Task ReadConsoleInput()
|
||||||
{
|
{
|
||||||
string userInput;
|
string lastCommand;
|
||||||
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
|
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
|
||||||
|
|
||||||
do
|
try
|
||||||
{
|
{
|
||||||
userInput = Console.ReadLine();
|
while (!ServerManager.CancellationToken.IsCancellationRequested)
|
||||||
|
|
||||||
if (userInput?.ToLower() == "quit")
|
|
||||||
{
|
{
|
||||||
ServerManager.Stop();
|
lastCommand = await Console.In.ReadLineAsync();
|
||||||
}
|
|
||||||
|
|
||||||
if (ServerManager.Servers.Count == 0)
|
if (lastCommand?.Length > 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
|
if (lastCommand?.Length > 0)
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userInput?.Length > 0)
|
|
||||||
{
|
{
|
||||||
GameEvent E = new GameEvent()
|
GameEvent E = new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Command,
|
Type = GameEvent.EventType.Command,
|
||||||
Data = userInput,
|
Data = lastCommand,
|
||||||
Origin = Origin,
|
Origin = Origin,
|
||||||
Owner = ServerManager.Servers[0]
|
Owner = ServerManager.Servers[0]
|
||||||
};
|
};
|
||||||
|
|
||||||
ServerManager.GetEventHandler().AddEvent(E);
|
ServerManager.GetEventHandler().AddEvent(E);
|
||||||
await E.WaitAsync(30 * 1000);
|
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
|
||||||
}
|
|
||||||
Console.Write('>');
|
Console.Write('>');
|
||||||
|
}
|
||||||
} while (ServerManager.Running);
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
/// <summary>
|
||||||
|
/// Configures the dependency injection services
|
||||||
|
/// </summary>
|
||||||
|
private static IServiceCollection ConfigureServices()
|
||||||
{
|
{
|
||||||
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"];
|
var defaultLogger = new Logger("IW4MAdmin-Manager");
|
||||||
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
|
var pluginImporter = new PluginImporter(defaultLogger);
|
||||||
|
|
||||||
Console.WriteLine(failMessage);
|
var serviceCollection = new ServiceCollection();
|
||||||
while (e.InnerException != null)
|
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
|
||||||
|
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>)
|
||||||
|
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
|
||||||
|
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration())
|
||||||
|
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
|
||||||
|
.AddSingleton<ILogger>(_serviceProvider => defaultLogger)
|
||||||
|
.AddSingleton<IPluginImporter, PluginImporter>()
|
||||||
|
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
|
||||||
|
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
|
||||||
|
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
|
||||||
|
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
|
||||||
|
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
|
||||||
|
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
|
||||||
|
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
|
||||||
|
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
|
||||||
|
.AddSingleton(_serviceProvider =>
|
||||||
{
|
{
|
||||||
e = e.InnerException;
|
var config = _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration();
|
||||||
}
|
return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
|
||||||
Console.WriteLine(e.Message);
|
customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
|
||||||
Console.WriteLine(exitMessage);
|
})
|
||||||
Console.ReadKey();
|
.AddSingleton<IManager, ApplicationManager>();
|
||||||
return;
|
|
||||||
|
// register the native commands
|
||||||
|
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
|
||||||
|
.Where(_command => _command.BaseType == typeof(Command)))
|
||||||
|
{
|
||||||
|
defaultLogger.WriteInfo($"Registered native command type {commandType.Name}");
|
||||||
|
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
|
// register the plugin implementations
|
||||||
|
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
|
||||||
|
foreach (var pluginType in pluginImplementations.Item1)
|
||||||
{
|
{
|
||||||
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
|
defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}");
|
||||||
|
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
|
||||||
}
|
}
|
||||||
|
|
||||||
OnShutdownComplete.Reset();
|
// register the plugin commands
|
||||||
ServerManager.Start();
|
foreach (var commandType in pluginImplementations.Item2)
|
||||||
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
|
{
|
||||||
OnShutdownComplete.Set();
|
defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}");
|
||||||
|
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnCancelKey(object sender, ConsoleCancelEventArgs e)
|
// register any script plugins
|
||||||
|
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
|
||||||
{
|
{
|
||||||
ServerManager.Stop();
|
serviceCollection.AddSingleton(scriptPlugin);
|
||||||
OnShutdownComplete.Wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CheckDirectories()
|
// register any eventable types
|
||||||
|
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
|
||||||
|
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
|
||||||
|
.Union(pluginImplementations
|
||||||
|
.Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
|
||||||
|
.Distinct()
|
||||||
|
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
|
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
|
||||||
{
|
serviceCollection.AddSingleton(instance);
|
||||||
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
|
return serviceCollection;
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,32 @@ namespace IW4MAdmin.Application.Migration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
class ConfigurationMigration
|
class ConfigurationMigration
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ensures required directories are created
|
||||||
|
/// </summary>
|
||||||
|
public static void CheckDirectories()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Localization")))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Localization"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// moves existing configs from the root folder into a configs folder
|
/// moves existing configs from the root folder into a configs folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
72
Application/Misc/BaseConfigurationHandler.cs
Normal file
72
Application/Misc/BaseConfigurationHandler.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// default implementation of IConfigurationHandler
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">base configuration type</typeparam>
|
||||||
|
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration
|
||||||
|
{
|
||||||
|
T _configuration;
|
||||||
|
|
||||||
|
public BaseConfigurationHandler(string fn)
|
||||||
|
{
|
||||||
|
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fn}.json");
|
||||||
|
Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FileName { get; }
|
||||||
|
|
||||||
|
public void Build()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configContent = File.ReadAllText(FileName);
|
||||||
|
_configuration = JsonConvert.DeserializeObject<T>(configContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
_configuration = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
|
||||||
|
{
|
||||||
|
Errors = new[] { e.Message },
|
||||||
|
ConfigurationFileName = FileName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Save()
|
||||||
|
{
|
||||||
|
var settings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
};
|
||||||
|
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
|
||||||
|
|
||||||
|
var appConfigJSON = JsonConvert.SerializeObject(_configuration, settings);
|
||||||
|
return File.WriteAllTextAsync(FileName, appConfigJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Configuration()
|
||||||
|
{
|
||||||
|
return _configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(T config)
|
||||||
|
{
|
||||||
|
_configuration = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
Application/Misc/EventProfiler.cs
Normal file
63
Application/Misc/EventProfiler.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
internal class EventPerformance
|
||||||
|
{
|
||||||
|
public long ExecutionTime { get; set; }
|
||||||
|
public GameEvent Event { get; set; }
|
||||||
|
public string EventInfo => $"{Event.Type}, {Event.FailReason}, {Event.IsBlocking}, {Event.Data}, {Event.Message}, {Event.Extra}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DuplicateKeyComparer<TKey> : IComparer<TKey> where TKey : IComparable
|
||||||
|
{
|
||||||
|
public int Compare(TKey x, TKey y)
|
||||||
|
{
|
||||||
|
int result = x.CompareTo(y);
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
return 1;
|
||||||
|
else
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EventProfiler
|
||||||
|
{
|
||||||
|
public double AverageEventTime { get; private set; }
|
||||||
|
public double MaxEventTime => Events.Values.Last().ExecutionTime;
|
||||||
|
public double MinEventTime => Events.Values[0].ExecutionTime;
|
||||||
|
public int TotalEventCount => Events.Count;
|
||||||
|
public SortedList<long, EventPerformance> Events { get; private set; } = new SortedList<long, EventPerformance>(new DuplicateKeyComparer<long>());
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public EventProfiler(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Profile(DateTime start, DateTime end, GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
_logger.WriteDebug($"Starting profile of event {gameEvent.Id}");
|
||||||
|
long executionTime = (long)Math.Round((end - start).TotalMilliseconds);
|
||||||
|
|
||||||
|
var perf = new EventPerformance()
|
||||||
|
{
|
||||||
|
Event = gameEvent,
|
||||||
|
ExecutionTime = executionTime
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (Events)
|
||||||
|
{
|
||||||
|
Events.Add(executionTime, perf);
|
||||||
|
}
|
||||||
|
|
||||||
|
AverageEventTime = (AverageEventTime * (TotalEventCount - 1) + executionTime) / TotalEventCount;
|
||||||
|
_logger.WriteDebug($"Finished profile of event {gameEvent.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Application/Misc/LogPathGeneratorInfo.cs
Normal file
45
Application/Misc/LogPathGeneratorInfo.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// dto class for handling log path generation
|
||||||
|
/// </summary>
|
||||||
|
public class LogPathGeneratorInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// directory under the paths where data comes from by default
|
||||||
|
/// <remarks>fs_basegame</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public string BaseGameDirectory { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// base game root path
|
||||||
|
/// <remarks>fs_basepath</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public string BasePathDirectory { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// overide game directory
|
||||||
|
/// <remarks>plugin driven</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public string GameDirectory { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// game director
|
||||||
|
/// <remarks>fs_game</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public string ModDirectory { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// log file name
|
||||||
|
/// <remarks>g_log</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public string LogFile { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// indicates if running on windows
|
||||||
|
/// </summary>
|
||||||
|
public bool IsWindows { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
using SharedLibraryCore;
|
using IW4MAdmin.Application.IO;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
class Logger : SharedLibraryCore.Interfaces.ILogger
|
public class Logger : ILogger
|
||||||
{
|
{
|
||||||
enum LogType
|
enum LogType
|
||||||
{
|
{
|
||||||
@ -19,16 +21,21 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly string FileName;
|
readonly string FileName;
|
||||||
readonly SemaphoreSlim OnLogWriting;
|
readonly ReaderWriterLockSlim WritingLock;
|
||||||
static readonly short MAX_LOG_FILES = 10;
|
static readonly short MAX_LOG_FILES = 10;
|
||||||
|
|
||||||
public Logger(string fn)
|
public Logger(string fn)
|
||||||
{
|
{
|
||||||
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
|
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
|
||||||
OnLogWriting = new SemaphoreSlim(1, 1);
|
WritingLock = new ReaderWriterLockSlim();
|
||||||
RotateLogs();
|
RotateLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Logger()
|
||||||
|
{
|
||||||
|
WritingLock.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// rotates logs when log is initialized
|
/// rotates logs when log is initialized
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -55,9 +62,10 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
void Write(string msg, LogType type)
|
void Write(string msg, LogType type)
|
||||||
{
|
{
|
||||||
OnLogWriting.Wait();
|
WritingLock.EnterWriteLock();
|
||||||
|
|
||||||
string stringType = type.ToString();
|
string stringType = type.ToString();
|
||||||
|
msg = msg.StripColors();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -71,12 +79,12 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
|
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
|
||||||
Console.WriteLine(LogLine);
|
Console.WriteLine(msg);
|
||||||
File.AppendAllText(FileName, LogLine + Environment.NewLine);
|
|
||||||
#else
|
#else
|
||||||
if (type == LogType.Error || type == LogType.Verbose)
|
if (type == LogType.Error || type == LogType.Verbose)
|
||||||
|
{
|
||||||
Console.WriteLine(LogLine);
|
Console.WriteLine(LogLine);
|
||||||
//if (type != LogType.Debug)
|
}
|
||||||
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -87,7 +95,7 @@ namespace IW4MAdmin.Application
|
|||||||
Console.WriteLine(ex.GetExceptionInfo());
|
Console.WriteLine(ex.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
OnLogWriting.Release(1);
|
WritingLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteVerbose(string msg)
|
public void WriteVerbose(string msg)
|
||||||
|
74
Application/Misc/MiddlewareActionHandler.cs
Normal file
74
Application/Misc/MiddlewareActionHandler.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
class MiddlewareActionHandler : IMiddlewareActionHandler
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, IList<object>> _actions;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public MiddlewareActionHandler(ILogger logger)
|
||||||
|
{
|
||||||
|
_actions = new Dictionary<string, IList<object>>();
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the action with the given name
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Execution return type</typeparam>
|
||||||
|
/// <param name="value">Input value</param>
|
||||||
|
/// <param name="name">Name of action to execute</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<T> Execute<T>(T value, string name = null)
|
||||||
|
{
|
||||||
|
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
|
||||||
|
|
||||||
|
if (_actions.ContainsKey(key))
|
||||||
|
{
|
||||||
|
foreach (var action in _actions[key])
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = await ((IMiddlewareAction<T>)action).Invoke(value);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.WriteWarning($"Failed to invoke middleware action {name}");
|
||||||
|
_logger.WriteDebug(e.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers an action by name
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="actionType">Action type specifier</param>
|
||||||
|
/// <param name="action">Action to perform</param>
|
||||||
|
/// <param name="name">Name of action</param>
|
||||||
|
public void Register<T>(T actionType, IMiddlewareAction<T> action, string name = null)
|
||||||
|
{
|
||||||
|
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
|
||||||
|
|
||||||
|
if (_actions.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_actions[key].Add(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_actions.Add(key, new[] { action });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Application/Misc/ParserMatchResult.cs
Normal file
21
Application/Misc/ParserMatchResult.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of the IMatchResult
|
||||||
|
/// used to hold matching results
|
||||||
|
/// </summary>
|
||||||
|
public class ParserMatchResult : IMatchResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// array of matched pattern groups
|
||||||
|
/// </summary>
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// indicates if the match succeeded
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; }
|
||||||
|
}
|
||||||
|
}
|
88
Application/Misc/PluginImporter.cs
Normal file
88
Application/Misc/PluginImporter.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Linq;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IPluginImporter
|
||||||
|
/// discovers plugins and script plugins
|
||||||
|
/// </summary>
|
||||||
|
public class PluginImporter : IPluginImporter
|
||||||
|
{
|
||||||
|
private static readonly string PLUGIN_DIR = "Plugins";
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public PluginImporter(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// discovers all the script plugins in the plugins dir
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IEnumerable<IPlugin> DiscoverScriptPlugins()
|
||||||
|
{
|
||||||
|
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
|
||||||
|
|
||||||
|
if (Directory.Exists(pluginDir))
|
||||||
|
{
|
||||||
|
string[] scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js");
|
||||||
|
|
||||||
|
_logger.WriteInfo($"Discovered {scriptPluginFiles.Length} potential script plugins");
|
||||||
|
|
||||||
|
if (scriptPluginFiles.Length > 0)
|
||||||
|
{
|
||||||
|
foreach (string fileName in scriptPluginFiles)
|
||||||
|
{
|
||||||
|
_logger.WriteInfo($"Discovered script plugin {fileName}");
|
||||||
|
var plugin = new ScriptPlugin(fileName);
|
||||||
|
yield return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// discovers all the C# assembly plugins and commands
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public (IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
|
||||||
|
{
|
||||||
|
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
|
||||||
|
var pluginTypes = Enumerable.Empty<Type>();
|
||||||
|
var commandTypes = Enumerable.Empty<Type>();
|
||||||
|
|
||||||
|
if (Directory.Exists(pluginDir))
|
||||||
|
{
|
||||||
|
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
|
||||||
|
_logger.WriteInfo($"Discovered {dllFileNames.Length} potential plugin assemblies");
|
||||||
|
|
||||||
|
if (dllFileNames.Length > 0)
|
||||||
|
{
|
||||||
|
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name));
|
||||||
|
|
||||||
|
pluginTypes = assemblies
|
||||||
|
.SelectMany(_asm => _asm.GetTypes())
|
||||||
|
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
|
||||||
|
|
||||||
|
_logger.WriteInfo($"Discovered {pluginTypes.Count()} plugin implementations");
|
||||||
|
|
||||||
|
commandTypes = assemblies
|
||||||
|
.SelectMany(_asm => _asm.GetTypes())
|
||||||
|
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
|
||||||
|
|
||||||
|
_logger.WriteInfo($"Discovered {commandTypes.Count()} plugin commands");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (pluginTypes, commandTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
197
Application/Misc/ScriptPlugin.cs
Normal file
197
Application/Misc/ScriptPlugin.cs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
using Jint;
|
||||||
|
using Microsoft.CSharp.RuntimeBinder;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IPlugin
|
||||||
|
/// used to proxy script plugin requests
|
||||||
|
/// </summary>
|
||||||
|
public class ScriptPlugin : IPlugin
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public float Version { get; set; }
|
||||||
|
|
||||||
|
public string Author { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// indicates if the plugin is a parser
|
||||||
|
/// </summary>
|
||||||
|
public bool IsParser { get; private set; }
|
||||||
|
|
||||||
|
public FileSystemWatcher Watcher { get; private set; }
|
||||||
|
|
||||||
|
private Engine _scriptEngine;
|
||||||
|
private readonly string _fileName;
|
||||||
|
private readonly SemaphoreSlim _onProcessing;
|
||||||
|
private bool successfullyLoaded;
|
||||||
|
|
||||||
|
public ScriptPlugin(string filename, string workingDirectory = null)
|
||||||
|
{
|
||||||
|
_fileName = filename;
|
||||||
|
Watcher = new FileSystemWatcher()
|
||||||
|
{
|
||||||
|
Path = workingDirectory == null ? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}" : workingDirectory,
|
||||||
|
NotifyFilter = NotifyFilters.Size,
|
||||||
|
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
|
||||||
|
};
|
||||||
|
|
||||||
|
Watcher.EnableRaisingEvents = true;
|
||||||
|
_onProcessing = new SemaphoreSlim(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScriptPlugin()
|
||||||
|
{
|
||||||
|
Watcher.Dispose();
|
||||||
|
_onProcessing.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Initialize(IManager manager)
|
||||||
|
{
|
||||||
|
await _onProcessing.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// for some reason we get an event trigger when the file is not finished being modified.
|
||||||
|
// this must have been a change in .NET CORE 3.x
|
||||||
|
// so if the new file is empty we can't process it yet
|
||||||
|
if (new FileInfo(_fileName).Length == 0L)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool firstRun = _scriptEngine == null;
|
||||||
|
|
||||||
|
// it's been loaded before so we need to call the unload event
|
||||||
|
if (!firstRun)
|
||||||
|
{
|
||||||
|
await OnUnloadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
successfullyLoaded = false;
|
||||||
|
string script;
|
||||||
|
|
||||||
|
using (var stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
|
{
|
||||||
|
using (var reader = new StreamReader(stream, Encoding.Default))
|
||||||
|
{
|
||||||
|
script = await reader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_scriptEngine = new Engine(cfg =>
|
||||||
|
cfg.AllowClr(new[]
|
||||||
|
{
|
||||||
|
typeof(System.Net.Http.HttpClient).Assembly,
|
||||||
|
typeof(EFClient).Assembly,
|
||||||
|
typeof(Utilities).Assembly,
|
||||||
|
typeof(Encoding).Assembly
|
||||||
|
})
|
||||||
|
.CatchClrExceptions());
|
||||||
|
|
||||||
|
_scriptEngine.Execute(script);
|
||||||
|
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
|
||||||
|
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
|
||||||
|
|
||||||
|
Author = pluginObject.author;
|
||||||
|
Name = pluginObject.name;
|
||||||
|
Version = (float)pluginObject.version;
|
||||||
|
|
||||||
|
await OnLoadAsync(manager);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (pluginObject.isParser)
|
||||||
|
{
|
||||||
|
IsParser = true;
|
||||||
|
IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
|
||||||
|
IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
|
||||||
|
manager.AdditionalEventParsers.Add(eventParser);
|
||||||
|
manager.AdditionalRConParsers.Add(rconParser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (RuntimeBinderException) { }
|
||||||
|
|
||||||
|
if (!firstRun)
|
||||||
|
{
|
||||||
|
await OnLoadAsync(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
successfullyLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onProcessing.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onProcessing.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnEventAsync(GameEvent E, Server S)
|
||||||
|
{
|
||||||
|
if (successfullyLoaded)
|
||||||
|
{
|
||||||
|
await _onProcessing.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_scriptEngine.SetValue("_gameEvent", E);
|
||||||
|
_scriptEngine.SetValue("_server", S);
|
||||||
|
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
|
||||||
|
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onProcessing.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onProcessing.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnLoadAsync(IManager manager)
|
||||||
|
{
|
||||||
|
manager.GetLogger(0).WriteDebug($"OnLoad executing for {Name}");
|
||||||
|
_scriptEngine.SetValue("_manager", manager);
|
||||||
|
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnTickAsync(Server S)
|
||||||
|
{
|
||||||
|
_scriptEngine.SetValue("_server", S);
|
||||||
|
return Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnUnloadAsync()
|
||||||
|
{
|
||||||
|
if (successfullyLoaded)
|
||||||
|
{
|
||||||
|
await Task.FromResult(_scriptEngine.Execute("plugin.onUnloadAsync()").GetCompletionValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
Application/RCon/ConnectionState.cs
Normal file
31
Application/RCon/ConnectionState.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.RCon
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// used to keep track of the udp connection state
|
||||||
|
/// </summary>
|
||||||
|
internal class ConnectionState
|
||||||
|
{
|
||||||
|
~ConnectionState()
|
||||||
|
{
|
||||||
|
OnComplete.Dispose();
|
||||||
|
OnSentData.Dispose();
|
||||||
|
OnReceivedData.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ConnectionAttempts { get; set; }
|
||||||
|
const int BufferSize = 4096;
|
||||||
|
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
|
||||||
|
public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
|
||||||
|
public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false);
|
||||||
|
public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false);
|
||||||
|
public List<int> BytesReadPerSegment { get; set; } = new List<int>();
|
||||||
|
public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs();
|
||||||
|
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs();
|
||||||
|
public DateTime LastQuery { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
361
Application/RCon/RConConnection.cs
Normal file
361
Application/RCon/RConConnection.cs
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.RCon;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.RCon
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IRConConnection
|
||||||
|
/// </summary>
|
||||||
|
public class RConConnection : IRConConnection
|
||||||
|
{
|
||||||
|
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>();
|
||||||
|
public IPEndPoint Endpoint { get; private set; }
|
||||||
|
public string RConPassword { get; private set; }
|
||||||
|
|
||||||
|
private IRConParserConfiguration config;
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly Encoding _gameEncoding;
|
||||||
|
|
||||||
|
public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding)
|
||||||
|
{
|
||||||
|
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
|
||||||
|
_gameEncoding = gameEncoding;
|
||||||
|
RConPassword = password;
|
||||||
|
_log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetConfiguration(IRConParserConfiguration config)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
|
||||||
|
{
|
||||||
|
if (!ActiveQueries.ContainsKey(this.Endpoint))
|
||||||
|
{
|
||||||
|
ActiveQueries.TryAdd(this.Endpoint, new ConnectionState());
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectionState = ActiveQueries[this.Endpoint];
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
_log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]");
|
||||||
|
#endif
|
||||||
|
// enter the semaphore so only one query is sent at a time per server.
|
||||||
|
await connectionState.OnComplete.WaitAsync();
|
||||||
|
|
||||||
|
var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
|
||||||
|
|
||||||
|
if (timeSinceLastQuery < StaticHelpers.FloodProtectionInterval)
|
||||||
|
{
|
||||||
|
await Task.Delay(StaticHelpers.FloodProtectionInterval - (int)timeSinceLastQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState.LastQuery = DateTime.Now;
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
_log.WriteDebug($"Semaphore has been released [{this.Endpoint}]");
|
||||||
|
_log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
byte[] payload = null;
|
||||||
|
bool waitForResponse = config.WaitForResponse;
|
||||||
|
|
||||||
|
string convertEncoding(string text)
|
||||||
|
{
|
||||||
|
byte[] convertedBytes = Utilities.EncodingType.GetBytes(text);
|
||||||
|
return _gameEncoding.GetString(convertedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string convertedRConPassword = convertEncoding(RConPassword);
|
||||||
|
string convertedParameters = convertEncoding(parameters);
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case StaticHelpers.QueryType.GET_DVAR:
|
||||||
|
waitForResponse |= true;
|
||||||
|
payload = string.Format(config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
|
||||||
|
break;
|
||||||
|
case StaticHelpers.QueryType.SET_DVAR:
|
||||||
|
payload = string.Format(config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
|
||||||
|
break;
|
||||||
|
case StaticHelpers.QueryType.COMMAND:
|
||||||
|
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(Convert.ToByte).ToArray();
|
||||||
|
break;
|
||||||
|
case StaticHelpers.QueryType.GET_INFO:
|
||||||
|
waitForResponse |= true;
|
||||||
|
payload = (config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
|
||||||
|
break;
|
||||||
|
case StaticHelpers.QueryType.COMMAND_STATUS:
|
||||||
|
waitForResponse |= true;
|
||||||
|
payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this happens when someone tries to send something that can't be converted into a 7 bit character set
|
||||||
|
// e.g: emoji -> windows-1252
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
connectionState.OnComplete.Release(1);
|
||||||
|
throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[][] response = null;
|
||||||
|
|
||||||
|
retrySend:
|
||||||
|
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
|
||||||
|
{
|
||||||
|
DontFragment = true,
|
||||||
|
Ttl = 100,
|
||||||
|
ExclusiveAddressUse = true,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
connectionState.SendEventArgs.UserToken = socket;
|
||||||
|
connectionState.OnSentData.Reset();
|
||||||
|
connectionState.OnReceivedData.Reset();
|
||||||
|
connectionState.ConnectionAttempts++;
|
||||||
|
connectionState.BytesReadPerSegment.Clear();
|
||||||
|
#if DEBUG == true
|
||||||
|
_log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})");
|
||||||
|
#endif
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await SendPayloadAsync(payload, waitForResponse);
|
||||||
|
|
||||||
|
if ((response.Length == 0 || response[0].Length == 0) && waitForResponse)
|
||||||
|
{
|
||||||
|
throw new NetworkException("Expected response but got 0 bytes back");
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState.ConnectionAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails)
|
||||||
|
{
|
||||||
|
await Task.Delay(StaticHelpers.FloodProtectionInterval);
|
||||||
|
goto retrySend;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"].FormatExt(Endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (connectionState.OnComplete.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
connectionState.OnComplete.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Length == 0)
|
||||||
|
{
|
||||||
|
_log.WriteWarning($"Received empty response for request [{type.ToString()}, {parameters}, {Endpoint.ToString()}]");
|
||||||
|
return new string[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
|
||||||
|
ReassembleSegmentedStatus(response) :
|
||||||
|
_gameEncoding.GetString(response[0]) + '\n';
|
||||||
|
|
||||||
|
// note: not all games respond if the pasword is wrong or not set
|
||||||
|
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
|
||||||
|
{
|
||||||
|
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseString.Contains("rcon_password"))
|
||||||
|
{
|
||||||
|
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseString.Contains(config.ServerNotRunningResponse))
|
||||||
|
{
|
||||||
|
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : config.CommandPrefixes.RConResponse);
|
||||||
|
|
||||||
|
if (headerSplit.Length != 2)
|
||||||
|
{
|
||||||
|
throw new NetworkException("Unexpected response header from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
return splitResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// reassembles broken status segments into the 'correct' ordering
|
||||||
|
/// <remarks>this is primarily for T7, and is really only reliable for 2 segments</remarks>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segments">array of segmented byte arrays</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string ReassembleSegmentedStatus(byte[][] segments)
|
||||||
|
{
|
||||||
|
var splitStatusStrings = new List<string>();
|
||||||
|
|
||||||
|
foreach (byte[] segment in segments)
|
||||||
|
{
|
||||||
|
string responseString = _gameEncoding.GetString(segment, 0, segment.Length);
|
||||||
|
var statusHeaderMatch = config.StatusHeader.PatternMatcher.Match(responseString);
|
||||||
|
if (statusHeaderMatch.Success)
|
||||||
|
{
|
||||||
|
splitStatusStrings.Insert(0, responseString.TrimEnd('\0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
splitStatusStrings.Add(responseString.Replace(config.CommandPrefixes.RConResponse, "").TrimEnd('\0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("", splitStatusStrings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse)
|
||||||
|
{
|
||||||
|
var connectionState = ActiveQueries[this.Endpoint];
|
||||||
|
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
|
||||||
|
|
||||||
|
if (connectionState.ReceiveEventArgs.RemoteEndPoint == null &&
|
||||||
|
connectionState.SendEventArgs.RemoteEndPoint == null)
|
||||||
|
{
|
||||||
|
// setup the event handlers only once because we're reusing the event args
|
||||||
|
connectionState.SendEventArgs.Completed += OnDataSent;
|
||||||
|
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
|
||||||
|
connectionState.SendEventArgs.RemoteEndPoint = this.Endpoint;
|
||||||
|
connectionState.ReceiveEventArgs.RemoteEndPoint = this.Endpoint;
|
||||||
|
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
|
||||||
|
connectionState.SendEventArgs.DisconnectReuseSocket = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState.SendEventArgs.SetBuffer(payload);
|
||||||
|
|
||||||
|
// send the data to the server
|
||||||
|
bool sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
|
||||||
|
|
||||||
|
if (sendDataPending)
|
||||||
|
{
|
||||||
|
// the send has not been completed asyncronously
|
||||||
|
if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout)))
|
||||||
|
{
|
||||||
|
rconSocket.Close();
|
||||||
|
throw new NetworkException("Timed out sending data", rconSocket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!waitForResponse)
|
||||||
|
{
|
||||||
|
return new byte[0][];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
|
||||||
|
|
||||||
|
// get our response back
|
||||||
|
bool receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
|
||||||
|
|
||||||
|
if (receiveDataPending)
|
||||||
|
{
|
||||||
|
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000)))
|
||||||
|
{
|
||||||
|
rconSocket.Close();
|
||||||
|
throw new NetworkException("Timed out waiting for response", rconSocket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rconSocket.Close();
|
||||||
|
|
||||||
|
var responseList = new List<byte[]>();
|
||||||
|
int totalBytesRead = 0;
|
||||||
|
|
||||||
|
foreach (int bytesRead in connectionState.BytesReadPerSegment)
|
||||||
|
{
|
||||||
|
responseList.Add(connectionState.ReceiveBuffer
|
||||||
|
.Skip(totalBytesRead)
|
||||||
|
.Take(bytesRead)
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
totalBytesRead += bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseList.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
|
||||||
|
{
|
||||||
|
#if DEBUG == true
|
||||||
|
_log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// this occurs when we close the socket
|
||||||
|
if (e.BytesTransferred == 0)
|
||||||
|
{
|
||||||
|
ActiveQueries[this.Endpoint].OnReceivedData.Set();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sender is Socket sock)
|
||||||
|
{
|
||||||
|
var state = ActiveQueries[this.Endpoint];
|
||||||
|
state.BytesReadPerSegment.Add(e.BytesTransferred);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// we still have available data so the payload was segmented
|
||||||
|
if (sock.Available > 0)
|
||||||
|
{
|
||||||
|
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, e.BytesTransferred, state.ReceiveBuffer.Length - e.BytesTransferred);
|
||||||
|
|
||||||
|
if (!sock.ReceiveAsync(state.ReceiveEventArgs))
|
||||||
|
{
|
||||||
|
#if DEBUG == true
|
||||||
|
_log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}");
|
||||||
|
#endif
|
||||||
|
// we need to increment this here because the callback isn't executed if there's no pending IO
|
||||||
|
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
|
||||||
|
ActiveQueries[this.Endpoint].OnReceivedData.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ActiveQueries[this.Endpoint].OnReceivedData.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
ActiveQueries[this.Endpoint].OnReceivedData.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataSent(object sender, SocketAsyncEventArgs e)
|
||||||
|
{
|
||||||
|
#if DEBUG == true
|
||||||
|
_log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}");
|
||||||
|
#endif
|
||||||
|
ActiveQueries[this.Endpoint].OnSentData.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,13 @@ using static SharedLibraryCore.Server;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
class BaseRConParser : IRConParser
|
public class BaseRConParser : IRConParser
|
||||||
{
|
{
|
||||||
public BaseRConParser()
|
private const int MAX_FAULTY_STATUS_LINES = 7;
|
||||||
|
|
||||||
|
public BaseRConParser(IParserRegexFactory parserRegexFactory)
|
||||||
{
|
{
|
||||||
Configuration = new DynamicRConParserConfiguration()
|
Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
|
||||||
{
|
{
|
||||||
CommandPrefixes = new CommandPrefix()
|
CommandPrefixes = new CommandPrefix()
|
||||||
{
|
{
|
||||||
@ -31,10 +33,12 @@ namespace IW4MAdmin.Application.RconParsers
|
|||||||
RConGetStatus = "ÿÿÿÿgetstatus",
|
RConGetStatus = "ÿÿÿÿgetstatus",
|
||||||
RConGetInfo = "ÿÿÿÿgetinfo",
|
RConGetInfo = "ÿÿÿÿgetinfo",
|
||||||
RConResponse = "ÿÿÿÿprint",
|
RConResponse = "ÿÿÿÿprint",
|
||||||
|
RconGetInfoResponseHeader = "ÿÿÿÿinfoResponse"
|
||||||
},
|
},
|
||||||
|
ServerNotRunningResponse = "Server is not running."
|
||||||
};
|
};
|
||||||
|
|
||||||
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) +(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
|
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback|unknown) +(-*[0-9]+) +([0-9]+) *$";
|
||||||
Configuration.Status.AddMapping(ParserRegex.GroupType.RConClientNumber, 1);
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConClientNumber, 1);
|
||||||
Configuration.Status.AddMapping(ParserRegex.GroupType.RConScore, 2);
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConScore, 2);
|
||||||
Configuration.Status.AddMapping(ParserRegex.GroupType.RConPing, 3);
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConPing, 3);
|
||||||
@ -48,130 +52,176 @@ namespace IW4MAdmin.Application.RconParsers
|
|||||||
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
|
||||||
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
|
||||||
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
|
||||||
|
|
||||||
|
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
|
||||||
|
Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)";
|
||||||
|
Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRConParserConfiguration Configuration { get; set; }
|
public IRConParserConfiguration Configuration { get; set; }
|
||||||
|
|
||||||
public string Version { get; set; } = "CoD";
|
public virtual string Version { get; set; } = "CoD";
|
||||||
public Game GameName { get; set; } = Game.COD;
|
public Game GameName { get; set; } = Game.COD;
|
||||||
public bool CanGenerateLogPath { get; set; } = true;
|
public bool CanGenerateLogPath { get; set; } = true;
|
||||||
|
public string Name { get; set; } = "Call of Duty";
|
||||||
|
|
||||||
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
|
||||||
{
|
{
|
||||||
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
|
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
|
||||||
return response.Skip(1).ToArray();
|
return response.Skip(1).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
|
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
|
||||||
{
|
{
|
||||||
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
|
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
|
||||||
string response = string.Join('\n', lineSplit.Skip(1));
|
string response = string.Join('\n', lineSplit).TrimEnd('\0');
|
||||||
|
|
||||||
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse))
|
|
||||||
{
|
|
||||||
throw new DvarException($"Could not retrieve DVAR \"{dvarName}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.Contains("Unknown command"))
|
|
||||||
{
|
|
||||||
throw new DvarException($"DVAR \"{dvarName}\" does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = Regex.Match(response, Configuration.Dvar.Pattern);
|
var match = Regex.Match(response, Configuration.Dvar.Pattern);
|
||||||
|
|
||||||
if (!match.Success)
|
if (response.Contains("Unknown command") ||
|
||||||
|
!match.Success)
|
||||||
|
{
|
||||||
|
if (fallbackValue != null)
|
||||||
{
|
{
|
||||||
throw new DvarException($"Could not retrieve DVAR \"{dvarName}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value.StripColors();
|
|
||||||
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value.StripColors();
|
|
||||||
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value.StripColors();
|
|
||||||
|
|
||||||
return new Dvar<T>()
|
return new Dvar<T>()
|
||||||
{
|
{
|
||||||
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value.StripColors(),
|
Name = dvarName,
|
||||||
Value = string.IsNullOrEmpty(value) ? default(T) : (T)Convert.ChangeType(value, typeof(T)),
|
Value = fallbackValue
|
||||||
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default(T) : (T)Convert.ChangeType(defaultValue, typeof(T)),
|
|
||||||
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default(T) : (T)Convert.ChangeType(latchedValue, typeof(T)),
|
|
||||||
Domain = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDomain]].Value.StripColors()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
|
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
|
||||||
{
|
|
||||||
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
|
|
||||||
return ClientsFromStatus(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value;
|
||||||
|
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value;
|
||||||
|
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value;
|
||||||
|
|
||||||
|
string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
|
||||||
|
|
||||||
|
value = removeTrailingColorCode(value);
|
||||||
|
defaultValue = removeTrailingColorCode(defaultValue);
|
||||||
|
latchedValue = removeTrailingColorCode(latchedValue);
|
||||||
|
|
||||||
|
return new Dvar<T>()
|
||||||
{
|
{
|
||||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0;
|
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value,
|
||||||
|
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
|
||||||
|
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
|
||||||
|
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
|
||||||
|
Domain = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDomain]].Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<(List<EFClient>, string)> GetStatusAsync(IRConConnection connection)
|
||||||
|
{
|
||||||
|
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
|
||||||
|
#if DEBUG
|
||||||
|
foreach (var line in response)
|
||||||
|
{
|
||||||
|
Console.WriteLine(line);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return (ClientsFromStatus(response), MapFromStatus(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapFromStatus(string[] response)
|
||||||
|
{
|
||||||
|
string map = null;
|
||||||
|
foreach (var line in response)
|
||||||
|
{
|
||||||
|
var regex = Regex.Match(line, Configuration.MapStatus.Pattern);
|
||||||
|
if (regex.Success)
|
||||||
|
{
|
||||||
|
map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
|
||||||
|
{
|
||||||
|
string dvarString = (dvarValue is string str)
|
||||||
|
? $"{dvarName} \"{str}\""
|
||||||
|
: $"{dvarName} {dvarValue.ToString()}";
|
||||||
|
|
||||||
|
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EFClient> ClientsFromStatus(string[] Status)
|
private List<EFClient> ClientsFromStatus(string[] Status)
|
||||||
{
|
{
|
||||||
List<EFClient> StatusPlayers = new List<EFClient>();
|
List<EFClient> StatusPlayers = new List<EFClient>();
|
||||||
|
|
||||||
if (Status.Length < 4)
|
bool parsedHeader = false;
|
||||||
{
|
|
||||||
throw new ServerException("Unexpected status response received");
|
|
||||||
}
|
|
||||||
|
|
||||||
int validMatches = 0;
|
|
||||||
foreach (string statusLine in Status)
|
foreach (string statusLine in Status)
|
||||||
{
|
{
|
||||||
string responseLine = statusLine.Trim();
|
string responseLine = statusLine.Trim();
|
||||||
|
|
||||||
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase);
|
if (Configuration.StatusHeader.PatternMatcher.Match(responseLine).Success)
|
||||||
|
|
||||||
if (regex.Success)
|
|
||||||
{
|
{
|
||||||
validMatches++;
|
parsedHeader = true;
|
||||||
int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value);
|
continue;
|
||||||
int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value);
|
}
|
||||||
|
|
||||||
|
var match = Configuration.Status.PatternMatcher.Match(responseLine);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
|
||||||
|
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
|
||||||
|
|
||||||
int ping = 999;
|
int ping = 999;
|
||||||
|
|
||||||
// their state can be CNCT, ZMBI etc
|
// their state can be CNCT, ZMBI etc
|
||||||
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3)
|
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
|
||||||
{
|
{
|
||||||
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
|
ping = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
long networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertLong();
|
long networkId;
|
||||||
string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.StripColors().Trim();
|
try
|
||||||
int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP();
|
{
|
||||||
|
networkId = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
|
||||||
|
int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
|
||||||
|
|
||||||
var client = new EFClient()
|
var client = new EFClient()
|
||||||
{
|
{
|
||||||
CurrentAlias = new EFAlias()
|
CurrentAlias = new EFAlias()
|
||||||
{
|
{
|
||||||
Name = name
|
Name = name,
|
||||||
|
IPAddress = ip
|
||||||
},
|
},
|
||||||
NetworkId = networkId,
|
NetworkId = networkId,
|
||||||
ClientNumber = clientNumber,
|
ClientNumber = clientNumber,
|
||||||
IPAddress = ip,
|
|
||||||
Ping = ping,
|
Ping = ping,
|
||||||
Score = score,
|
Score = score,
|
||||||
IsBot = ip == null,
|
|
||||||
State = EFClient.ClientState.Connecting
|
State = EFClient.ClientState.Connecting
|
||||||
};
|
};
|
||||||
|
|
||||||
//// they've not fully connected yet
|
#if DEBUG
|
||||||
//if (!client.IsBot && ping == 999)
|
if (client.NetworkId < 1000 && client.NetworkId > 0)
|
||||||
//{
|
{
|
||||||
// continue;
|
client.IPAddress = 2147483646;
|
||||||
//}
|
client.Ping = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
StatusPlayers.Add(client);
|
StatusPlayers.Add(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this happens if status is requested while map is rotating
|
// this can happen if status is requested while map is rotating and we get a log dump back
|
||||||
if (Status.Length > 5 && validMatches == 0)
|
if (!parsedHeader)
|
||||||
{
|
{
|
||||||
throw new ServerException("Server is rotating map");
|
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return StatusPlayers;
|
return StatusPlayers;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
namespace IW4MAdmin.Application.RconParsers
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// empty implementation of the IW4RConParser
|
/// empty implementation of the IW4RConParser
|
||||||
@ -6,5 +8,8 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
sealed internal class DynamicRConParser : BaseRConParser
|
sealed internal class DynamicRConParser : BaseRConParser
|
||||||
{
|
{
|
||||||
|
public DynamicRConParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using SharedLibraryCore;
|
using IW4MAdmin.Application.Factories;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.RCon;
|
using SharedLibraryCore.RCon;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
@ -8,11 +9,23 @@ namespace IW4MAdmin.Application.RconParsers
|
|||||||
/// generic implementation of the IRConParserConfiguration
|
/// generic implementation of the IRConParserConfiguration
|
||||||
/// allows script plugins to generate dynamic RCon configurations
|
/// allows script plugins to generate dynamic RCon configurations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
sealed internal class DynamicRConParserConfiguration : IRConParserConfiguration
|
public class DynamicRConParserConfiguration : IRConParserConfiguration
|
||||||
{
|
{
|
||||||
public CommandPrefix CommandPrefixes { get; set; }
|
public CommandPrefix CommandPrefixes { get; set; }
|
||||||
public ParserRegex Status { get; set; } = new ParserRegex();
|
public ParserRegex Status { get; set; }
|
||||||
public ParserRegex Dvar { get; set; } = new ParserRegex();
|
public ParserRegex MapStatus { get; set; }
|
||||||
|
public ParserRegex Dvar { get; set; }
|
||||||
|
public ParserRegex StatusHeader { get; set; }
|
||||||
|
public string ServerNotRunningResponse { get; set; }
|
||||||
public bool WaitForResponse { get; set; } = true;
|
public bool WaitForResponse { get; set; } = true;
|
||||||
|
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
|
||||||
|
|
||||||
|
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
|
||||||
|
{
|
||||||
|
Status = parserRegexFactory.CreateParserRegex();
|
||||||
|
MapStatus = parserRegexFactory.CreateParserRegex();
|
||||||
|
Dvar = parserRegexFactory.CreateParserRegex();
|
||||||
|
StatusHeader = parserRegexFactory.CreateParserRegex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
53
GameFiles/IW4x/userraw/scripts/_commands.gsc
Normal file
53
GameFiles/IW4x/userraw/scripts/_commands.gsc
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#include common_scripts\utility;
|
||||||
|
#include maps\mp\_utility;
|
||||||
|
#include maps\mp\gametypes\_hud_util;
|
||||||
|
#include maps\mp\gametypes\_playerlogic;
|
||||||
|
|
||||||
|
init()
|
||||||
|
{
|
||||||
|
SetDvarIfUninitialized( "sv_iw4madmin_command", "" );
|
||||||
|
level thread WaitForCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitForCommand()
|
||||||
|
{
|
||||||
|
level endon( "game_ended" );
|
||||||
|
|
||||||
|
for(;;)
|
||||||
|
{
|
||||||
|
commandInfo = strtok( getDvar("sv_iw4madmin_command"), ";" );
|
||||||
|
command = commandInfo[0];
|
||||||
|
|
||||||
|
switch( command )
|
||||||
|
{
|
||||||
|
case "alert":
|
||||||
|
//clientId alertType sound message
|
||||||
|
SendAlert( commandInfo[1], commandInfo[2], commandInfo[3], commandInfo[4] );
|
||||||
|
break;
|
||||||
|
case "killplayer":
|
||||||
|
// clientId
|
||||||
|
KillPlayer( commandInfo[1], commandInfo[2] );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDvar( "sv_iw4madmin_command", "" );
|
||||||
|
wait( 1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SendAlert( clientId, alertType, sound, message )
|
||||||
|
{
|
||||||
|
client = getPlayerFromClientNum( int( clientId ) );
|
||||||
|
client thread playLeaderDialogOnPlayer( sound, client.team );
|
||||||
|
client playLocalSound( sound );
|
||||||
|
client iPrintLnBold( "^1" + alertType + ": ^3" + message );
|
||||||
|
}
|
||||||
|
|
||||||
|
KillPlayer( targetId, originId)
|
||||||
|
{
|
||||||
|
target = getPlayerFromClientNum( int( targetId ) );
|
||||||
|
target suicide();
|
||||||
|
origin = getPlayerFromClientNum( int( originId ) );
|
||||||
|
|
||||||
|
iPrintLnBold("^1" + origin.name + " ^7killed ^5" + target.name);
|
||||||
|
}
|
256
GameFiles/IW4x/userraw/scripts/_customcallbacks.gsc
Normal file
256
GameFiles/IW4x/userraw/scripts/_customcallbacks.gsc
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPlayerConnect( player )
|
||||||
|
{
|
||||||
|
for( ;; )
|
||||||
|
{
|
||||||
|
level waittill( "connected", player );
|
||||||
|
player setClientDvar("cl_autorecord", 1);
|
||||||
|
player setClientDvar("cl_demosKeep", 200);
|
||||||
|
player thread waitForFrameThread();
|
||||||
|
player thread waitForAttack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForAttack()
|
||||||
|
{
|
||||||
|
self endon( "disconnect" );
|
||||||
|
|
||||||
|
self.lastAttackTime = 0;
|
||||||
|
|
||||||
|
for( ;; )
|
||||||
|
{
|
||||||
|
self notifyOnPlayerCommand( "player_shot", "+attack" );
|
||||||
|
self waittill( "player_shot" );
|
||||||
|
|
||||||
|
self.lastAttackTime = getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getHttpString( url )
|
||||||
|
{
|
||||||
|
request = httpGet( url );
|
||||||
|
request waittill( "done", success, data );
|
||||||
|
request destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
runRadarUpdates()
|
||||||
|
{
|
||||||
|
interval = int(getDvar("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 = int(getTime()) - int(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 = !isPlayer( attacker ) || isKillstreakWeapon( sWeapon );
|
||||||
|
|
||||||
|
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 )
|
||||||
|
{
|
||||||
|
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\_damage::Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime );
|
||||||
|
}
|
||||||
|
|
||||||
|
Callback_PlayerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration )
|
||||||
|
{
|
||||||
|
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
|
||||||
|
self maps\mp\gametypes\_damage::Callback_PlayerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
|
||||||
|
}
|
||||||
|
|
||||||
|
Callback_PlayerDisconnect()
|
||||||
|
{
|
||||||
|
level notify( "disconnected", self );
|
||||||
|
self maps\mp\gametypes\_playerlogic::Callback_PlayerDisconnect();
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
class LogReader(object):
|
class LogReader(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -8,7 +10,7 @@ class LogReader(object):
|
|||||||
# (if the time between checks is greater, ignore ) - in seconds
|
# (if the time between checks is greater, ignore ) - in seconds
|
||||||
self.max_file_time_change = 30
|
self.max_file_time_change = 30
|
||||||
|
|
||||||
def read_file(self, path):
|
def read_file(self, path, retrieval_key):
|
||||||
# this removes old entries that are no longer valid
|
# this removes old entries that are no longer valid
|
||||||
try:
|
try:
|
||||||
self._clear_old_logs()
|
self._clear_old_logs()
|
||||||
@ -22,57 +24,88 @@ class LogReader(object):
|
|||||||
|
|
||||||
# prevent traversing directories
|
# prevent traversing directories
|
||||||
if re.search('r^.+\.\.\\.+$', path):
|
if re.search('r^.+\.\.\\.+$', path):
|
||||||
return False
|
return self._generate_bad_response()
|
||||||
|
|
||||||
# must be a valid log path and log file
|
# must be a valid log path and log file
|
||||||
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
|
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
|
||||||
return False
|
return self._generate_bad_response()
|
||||||
|
|
||||||
# get the new file size
|
# get the new file size
|
||||||
new_file_size = self.file_length(path)
|
new_file_size = self.file_length(path)
|
||||||
|
|
||||||
# the log size was unable to be read (probably the wrong path)
|
# the log size was unable to be read (probably the wrong path)
|
||||||
if new_file_size < 0:
|
if new_file_size < 0:
|
||||||
return False
|
return self._generate_bad_response()
|
||||||
|
|
||||||
# this is the first time the log has been requested
|
next_retrieval_key = self._generate_key()
|
||||||
if path not in self.log_file_sizes:
|
|
||||||
self.log_file_sizes[path] = {
|
# this is the first time the key has been requested, so we need to the next one
|
||||||
'length' : new_file_size,
|
if retrieval_key not in self.log_file_sizes or int(time.time() - self.log_file_sizes[retrieval_key]['read']) > self.max_file_time_change:
|
||||||
'read': time.time()
|
print('retrieval key "%s" does not exist or is outdated' % retrieval_key)
|
||||||
|
last_log_info = {
|
||||||
|
'size' : new_file_size,
|
||||||
|
'previous_key' : None
|
||||||
}
|
}
|
||||||
return ''
|
else:
|
||||||
|
last_log_info = self.log_file_sizes[retrieval_key]
|
||||||
|
|
||||||
# grab the previous values
|
print('next key is %s' % next_retrieval_key)
|
||||||
last_length = self.log_file_sizes[path]['length']
|
expired_key = last_log_info['previous_key']
|
||||||
file_size_difference = new_file_size - last_length
|
print('expired key is %s' % expired_key)
|
||||||
|
|
||||||
# update the new size and actually read the data
|
# grab the previous value
|
||||||
self.log_file_sizes[path] = {
|
last_size = last_log_info['size']
|
||||||
'length': new_file_size,
|
file_size_difference = new_file_size - last_size
|
||||||
'read': time.time()
|
|
||||||
|
#print('generating info for next key %s' % next_retrieval_key)
|
||||||
|
|
||||||
|
# update the new size
|
||||||
|
self.log_file_sizes[next_retrieval_key] = {
|
||||||
|
'size' : new_file_size,
|
||||||
|
'read': time.time(),
|
||||||
|
'next_key': next_retrieval_key,
|
||||||
|
'previous_key': retrieval_key
|
||||||
}
|
}
|
||||||
|
|
||||||
new_log_info = self.get_file_lines(path, file_size_difference)
|
if expired_key in self.log_file_sizes:
|
||||||
return new_log_info
|
print('deleting expired key %s' % expired_key)
|
||||||
|
del self.log_file_sizes[expired_key]
|
||||||
|
|
||||||
def get_file_lines(self, path, length):
|
#print('reading %i bytes starting at %i' % (file_size_difference, last_size))
|
||||||
|
|
||||||
|
new_log_content = self.get_file_lines(path, last_size, file_size_difference)
|
||||||
|
return {
|
||||||
|
'content': new_log_content,
|
||||||
|
'next_key': next_retrieval_key
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_file_lines(self, path, start_position, length_to_read):
|
||||||
try:
|
try:
|
||||||
file_handle = open(path, 'rb')
|
file_handle = open(path, 'rb')
|
||||||
file_handle.seek(-length, 2)
|
file_handle.seek(start_position)
|
||||||
file_data = file_handle.read(length)
|
file_data = file_handle.read(length_to_read)
|
||||||
file_handle.close()
|
file_handle.close()
|
||||||
# using ignore errors omits the pesky 0xb2 bytes we're reading in for some reason
|
# using ignore errors omits the pesky 0xb2 bytes we're reading in for some reason
|
||||||
return file_data.decode('utf-8', errors='ignore')
|
return file_data.decode('utf-8', errors='ignore')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length))
|
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length_to_read))
|
||||||
print(e)
|
print(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _clear_old_logs(self):
|
def _clear_old_logs(self):
|
||||||
expired_logs = [path for path in self.log_file_sizes if int(time.time() - self.log_file_sizes[path]['read']) > self.max_file_time_change]
|
expired_logs = [path for path in self.log_file_sizes if int(time.time() - self.log_file_sizes[path]['read']) > self.max_file_time_change]
|
||||||
for log in expired_logs:
|
for key in expired_logs:
|
||||||
print('removing expired log {0}'.format(log))
|
print('removing expired log with key {0}'.format(key))
|
||||||
del self.log_file_sizes[log]
|
del self.log_file_sizes[key]
|
||||||
|
|
||||||
|
def _generate_bad_response(self):
|
||||||
|
return {
|
||||||
|
'content': None,
|
||||||
|
'next_key': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_key(self):
|
||||||
|
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||||
|
|
||||||
def file_length(self, path):
|
def file_length(self, path):
|
||||||
try:
|
try:
|
||||||
|
@ -3,12 +3,14 @@ from GameLogServer.log_reader import reader
|
|||||||
from base64 import urlsafe_b64decode
|
from base64 import urlsafe_b64decode
|
||||||
|
|
||||||
class LogResource(Resource):
|
class LogResource(Resource):
|
||||||
def get(self, path):
|
def get(self, path, retrieval_key):
|
||||||
path = urlsafe_b64decode(path).decode('utf-8')
|
path = urlsafe_b64decode(path).decode('utf-8')
|
||||||
log_info = reader.read_file(path)
|
log_info = reader.read_file(path, retrieval_key)
|
||||||
|
content = log_info['content']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success' : log_info is not False,
|
'success' : content is not None,
|
||||||
'length': 0 if log_info is False else len(log_info),
|
'length': 0 if content is None else len(content),
|
||||||
'data': log_info
|
'data': content,
|
||||||
|
'next_key': log_info['next_key']
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,5 @@ def init():
|
|||||||
log = logging.getLogger('werkzeug')
|
log = logging.getLogger('werkzeug')
|
||||||
log.setLevel(logging.ERROR)
|
log.setLevel(logging.ERROR)
|
||||||
api = Api(app)
|
api = Api(app)
|
||||||
api.add_resource(LogResource, '/log/<string:path>')
|
api.add_resource(LogResource, '/log/<string:path>/<string:retrieval_key>')
|
||||||
#api.add_resource(RestartResource, '/restart')
|
#api.add_resource(RestartResource, '/restart')
|
||||||
|
@ -9,4 +9,4 @@ pip==10.0.1
|
|||||||
pytz==2018.9
|
pytz==2018.9
|
||||||
setuptools==39.0.1
|
setuptools==39.0.1
|
||||||
six==1.12.0
|
six==1.12.0
|
||||||
Werkzeug==0.15.2
|
Werkzeug==0.16.0
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 15.0.26730.16
|
VisualStudioVersion = 16.0.29009.5
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
_commands.gsc = _commands.gsc
|
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
|
||||||
_customcallbacks.gsc = _customcallbacks.gsc
|
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
|
||||||
|
azure-pipelines.yml = azure-pipelines.yml
|
||||||
|
PostPublish.ps1 = PostPublish.ps1
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
RunPublishPre.cmd = RunPublishPre.cmd
|
RunPublishPre.cmd = RunPublishPre.cmd
|
||||||
RunPublishRelease.cmd = RunPublishRelease.cmd
|
RunPublishRelease.cmd = RunPublishRelease.cmd
|
||||||
@ -34,14 +36,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Test
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
|
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
|
||||||
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
|
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
|
||||||
|
Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js
|
||||||
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
|
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
|
||||||
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
|
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
|
||||||
|
Plugins\ScriptPlugins\ParserT7.js = Plugins\ScriptPlugins\ParserT7.js
|
||||||
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
|
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
|
||||||
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
|
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
|
||||||
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
|
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
|
||||||
@ -55,6 +57,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\Sta
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveRadar", "Plugins\LiveRadar\LiveRadar.csproj", "{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3065279E-17F0-4CE0-AF5B-014E04263D77}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationTests", "Tests\ApplicationTests\ApplicationTests.csproj", "{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -309,18 +317,6 @@ Global
|
|||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
@ -375,8 +371,8 @@ Global
|
|||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.Build.0 = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.Build.0 = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
@ -391,6 +387,53 @@ Global
|
|||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.Build.0 = Release|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.ActiveCfg = Release|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.Build.0 = Release|Any CPU
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -406,6 +449,8 @@ Global
|
|||||||
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678}
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678}
|
||||||
{F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
|
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
|
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B} = {3065279E-17F0-4CE0-AF5B-014E04263D77}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
||||||
|
@ -8,7 +8,7 @@ class History():
|
|||||||
self.server_history = list()
|
self.server_history = list()
|
||||||
|
|
||||||
def add_client_history(self, client_num):
|
def add_client_history(self, client_num):
|
||||||
if len(self.client_history) > 2880:
|
if len(self.client_history) > 20160:
|
||||||
self.client_history = self.client_history[1:]
|
self.client_history = self.client_history[1:]
|
||||||
self.client_history.append({
|
self.client_history.append({
|
||||||
'count' : client_num,
|
'count' : client_num,
|
||||||
@ -16,7 +16,7 @@ class History():
|
|||||||
})
|
})
|
||||||
|
|
||||||
def add_server_history(self, server_num):
|
def add_server_history(self, server_num):
|
||||||
if len(self.server_history) > 2880:
|
if len(self.server_history) > 20160:
|
||||||
self.server_history = self.server_history[1:]
|
self.server_history = self.server_history[1:]
|
||||||
self.server_history.append({
|
self.server_history.append({
|
||||||
'count' : server_num,
|
'count' : server_num,
|
||||||
@ -24,7 +24,7 @@ class History():
|
|||||||
})
|
})
|
||||||
|
|
||||||
def add_instance_history(self, instance_num):
|
def add_instance_history(self, instance_num):
|
||||||
if len(self.instance_history) > 2880:
|
if len(self.instance_history) > 20160:
|
||||||
self.instance_history = self.instance_history[1:]
|
self.instance_history = self.instance_history[1:]
|
||||||
self.instance_history.append({
|
self.instance_history.append({
|
||||||
'count' : instance_num,
|
'count' : instance_num,
|
||||||
|
@ -23,7 +23,7 @@ class ServerSchema(Schema):
|
|||||||
)
|
)
|
||||||
hostname = fields.String(
|
hostname = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Length(1, 64, 'invalid hostname')
|
validate=validate.Length(1, 128, 'invalid hostname')
|
||||||
)
|
)
|
||||||
clientnum = fields.Int(
|
clientnum = fields.Int(
|
||||||
required=True,
|
required=True,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12 col-sm-8 ml-auto mr-auto">
|
||||||
<figure>
|
<figure>
|
||||||
<div id="history_graph">{{history_graph|safe}}</div>
|
<div id="history_graph">{{history_graph|safe}}</div>
|
||||||
<figcaption class="float-right">
|
<figcaption class="float-right">
|
||||||
@ -15,10 +15,6 @@
|
|||||||
<span class="h4 text-muted">— {{server_count}} servers</span>
|
<span class="h4 text-muted">— {{server_count}} servers</span>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -23,4 +23,4 @@ six==1.11.0
|
|||||||
timeago==1.0.8
|
timeago==1.0.8
|
||||||
tzlocal==1.5.1
|
tzlocal==1.5.1
|
||||||
urllib3==1.24
|
urllib3==1.24
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.15.3
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2"/>
|
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||||
</ItemGroup>
|
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.10" PrivateAssets="All" />
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins""/>
|
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
|
||||||
<Exec Command="copy "$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll" "$(SolutionDir)BUILD\Plugins""/>
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -20,14 +20,19 @@ namespace AutomessageFeed
|
|||||||
|
|
||||||
public string Author => "RaidMax";
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
private Configuration _configuration;
|
|
||||||
private int _currentFeedItem;
|
private int _currentFeedItem;
|
||||||
|
private readonly IConfigurationHandler<Configuration> _configurationHandler;
|
||||||
|
|
||||||
|
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
|
||||||
|
{
|
||||||
|
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> GetNextFeedItem(Server server)
|
private async Task<string> GetNextFeedItem(Server server)
|
||||||
{
|
{
|
||||||
var items = new List<string>();
|
var items = new List<string>();
|
||||||
|
|
||||||
using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings() { Async = true }))
|
using (var reader = XmlReader.Create(_configurationHandler.Configuration().FeedUrl, new XmlReaderSettings() { Async = true }))
|
||||||
{
|
{
|
||||||
var feedReader = new RssFeedReader(reader);
|
var feedReader = new RssFeedReader(reader);
|
||||||
|
|
||||||
@ -43,7 +48,7 @@ namespace AutomessageFeed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
|
if (_currentFeedItem < items.Count && (_configurationHandler.Configuration().MaxFeedItems == 0 || _currentFeedItem < _configurationHandler.Configuration().MaxFeedItems))
|
||||||
{
|
{
|
||||||
_currentFeedItem++;
|
_currentFeedItem++;
|
||||||
return items[_currentFeedItem - 1];
|
return items[_currentFeedItem - 1];
|
||||||
@ -60,15 +65,12 @@ namespace AutomessageFeed
|
|||||||
|
|
||||||
public async Task OnLoadAsync(IManager manager)
|
public async Task OnLoadAsync(IManager manager)
|
||||||
{
|
{
|
||||||
var cfg = new BaseConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
|
if (_configurationHandler.Configuration() == null)
|
||||||
if (cfg.Configuration() == null)
|
|
||||||
{
|
{
|
||||||
cfg.Set((Configuration)new Configuration().Generate());
|
_configurationHandler.Set((Configuration)new Configuration().Generate());
|
||||||
await cfg.Save();
|
await _configurationHandler.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
_configuration = cfg.Configuration();
|
|
||||||
|
|
||||||
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
|
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ namespace AutomessageFeed
|
|||||||
|
|
||||||
public Task OnUnloadAsync()
|
public Task OnUnloadAsync()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace IW4ScriptCommands
|
|
||||||
{
|
|
||||||
class CommandInfo
|
|
||||||
{
|
|
||||||
public string Command { get; set; }
|
|
||||||
public int ClientNumber { get; set; }
|
|
||||||
public List<string> CommandArguments { get; set; } = new List<string>();
|
|
||||||
public override string ToString() => $"{Command};{ClientNumber},{string.Join(',', CommandArguments)}";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,200 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4ScriptCommands.Commands
|
|
||||||
{
|
|
||||||
class Balance
|
|
||||||
{
|
|
||||||
private class TeamAssignment
|
|
||||||
{
|
|
||||||
public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; }
|
|
||||||
public int Num { get; set; }
|
|
||||||
public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetTeamAssignments(EFClient client, bool isDisconnect, Server server, string teamsString = "")
|
|
||||||
{
|
|
||||||
var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(c => c.Split(','))
|
|
||||||
.Select(c => new TeamAssignment()
|
|
||||||
{
|
|
||||||
CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]),
|
|
||||||
Num = server.GetClientsAsList().FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0]))?.ClientNumber ?? -1,
|
|
||||||
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(server.Clients.FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0])).ClientId, server.EndPoint)
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// at least one team is full so we can't balance
|
|
||||||
if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(server.MaxClients / 2.0)
|
|
||||||
|| scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(server.MaxClients / 2.0))
|
|
||||||
{
|
|
||||||
// E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]);
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string> teamAssignments = new List<string>();
|
|
||||||
|
|
||||||
var _c = server.GetClientsAsList();
|
|
||||||
if (isDisconnect && client != null)
|
|
||||||
{
|
|
||||||
_c = _c.Where(c => c.ClientNumber != client.ClientNumber).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeClients = _c.Select(c => new TeamAssignment()
|
|
||||||
{
|
|
||||||
Num = c.ClientNumber,
|
|
||||||
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint),
|
|
||||||
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint).Team
|
|
||||||
})
|
|
||||||
.Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator)
|
|
||||||
.Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam)
|
|
||||||
.OrderByDescending(c => c.Stats.Performance)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var alliesTeam = scriptClientTeams
|
|
||||||
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies)
|
|
||||||
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var axisTeam = scriptClientTeams
|
|
||||||
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis)
|
|
||||||
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
while (activeClients.Count() > 0)
|
|
||||||
{
|
|
||||||
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
|
|
||||||
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
|
|
||||||
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
|
|
||||||
|
|
||||||
if (teamSizeDifference == 0)
|
|
||||||
{
|
|
||||||
if (performanceDisparity == 0)
|
|
||||||
{
|
|
||||||
alliesTeam.Add(activeClients.First());
|
|
||||||
activeClients.RemoveAt(0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (performanceDisparity > 0)
|
|
||||||
{
|
|
||||||
axisTeam.Add(activeClients.First());
|
|
||||||
activeClients.RemoveAt(0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
alliesTeam.Add(activeClients.First());
|
|
||||||
activeClients.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (teamSizeDifference > 0)
|
|
||||||
{
|
|
||||||
if (performanceDisparity > 0)
|
|
||||||
{
|
|
||||||
axisTeam.Add(activeClients.First());
|
|
||||||
activeClients.RemoveAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
axisTeam.Add(activeClients.Last());
|
|
||||||
activeClients.RemoveAt(activeClients.Count - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (performanceDisparity > 0)
|
|
||||||
{
|
|
||||||
alliesTeam.Add(activeClients.First());
|
|
||||||
activeClients.RemoveAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
alliesTeam.Add(activeClients.Last());
|
|
||||||
activeClients.RemoveAt(activeClients.Count - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1)
|
|
||||||
{
|
|
||||||
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
|
|
||||||
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
|
|
||||||
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
|
|
||||||
|
|
||||||
if (teamSizeDifference > 0)
|
|
||||||
{
|
|
||||||
if (performanceDisparity > 0)
|
|
||||||
{
|
|
||||||
axisTeam.Add(alliesTeam.First());
|
|
||||||
alliesTeam.RemoveAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
axisTeam.Add(alliesTeam.Last());
|
|
||||||
alliesTeam.RemoveAt(axisTeam.Count - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (performanceDisparity > 0)
|
|
||||||
{
|
|
||||||
alliesTeam.Add(axisTeam.Last());
|
|
||||||
axisTeam.RemoveAt(axisTeam.Count - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
alliesTeam.Add(axisTeam.First());
|
|
||||||
axisTeam.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var assignment in alliesTeam)
|
|
||||||
{
|
|
||||||
teamAssignments.Add($"{assignment.Num},2");
|
|
||||||
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var assignment in axisTeam)
|
|
||||||
{
|
|
||||||
teamAssignments.Add($"{assignment.Num},3");
|
|
||||||
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 &&
|
|
||||||
// axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0)
|
|
||||||
//{
|
|
||||||
// //E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]);
|
|
||||||
// return string.Empty;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//if (E.Origin?.Level > Player.Permission.Administrator)
|
|
||||||
//{
|
|
||||||
// E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}");
|
|
||||||
// E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}");
|
|
||||||
//}
|
|
||||||
|
|
||||||
//E.Origin.Tell("Balance command sent");
|
|
||||||
string args = string.Join(",", teamAssignments);
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
44
Plugins/IW4ScriptCommands/Commands/KillPlayerCommand.cs
Normal file
44
Plugins/IW4ScriptCommands/Commands/KillPlayerCommand.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Commands;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4ScriptCommands.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Example script command
|
||||||
|
/// </summary>
|
||||||
|
public class KillPlayerCommand : Command
|
||||||
|
{
|
||||||
|
public KillPlayerCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup)
|
||||||
|
{
|
||||||
|
Name = "killplayer";
|
||||||
|
Description = "kill a player";
|
||||||
|
Alias = "kp";
|
||||||
|
Permission = EFClient.Permission.Administrator;
|
||||||
|
RequiresTarget = true;
|
||||||
|
Arguments = new[]
|
||||||
|
{
|
||||||
|
new CommandArgument()
|
||||||
|
{
|
||||||
|
Name = "player",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
|
{
|
||||||
|
var cmd = new ScriptCommand()
|
||||||
|
{
|
||||||
|
CommandName = "killplayer",
|
||||||
|
ClientNumber = E.Target.ClientNumber,
|
||||||
|
CommandArguments = new[] { E.Origin.ClientNumber.ToString() }
|
||||||
|
};
|
||||||
|
|
||||||
|
await cmd.Execute(E.Owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,30 @@
|
|||||||
using IW4ScriptCommands.Commands;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace WebfrontCore.Controllers.API
|
namespace WebfrontCore.Controllers.API
|
||||||
{
|
{
|
||||||
[Route("api/gsc/[action]")]
|
[Route("api/gsc/[action]")]
|
||||||
public class GscApiController : ApiController
|
public class GscApiController : BaseController
|
||||||
{
|
{
|
||||||
|
public GscApiController(IManager manager) : base(manager)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// grabs basic info about the client from IW4MAdmin
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="networkId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("{networkId}")]
|
[HttpGet("{networkId}")]
|
||||||
public IActionResult ClientInfo(string networkId)
|
public IActionResult ClientInfo(string networkId)
|
||||||
{
|
{
|
||||||
|
long decimalNetworkId = networkId.ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber);
|
||||||
var clientInfo = Manager.GetActiveClients()
|
var clientInfo = Manager.GetActiveClients()
|
||||||
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
|
.FirstOrDefault(c => c.NetworkId == decimalNetworkId);
|
||||||
|
|
||||||
if (clientInfo != null)
|
if (clientInfo != null)
|
||||||
{
|
{
|
||||||
@ -33,22 +40,5 @@ namespace WebfrontCore.Controllers.API
|
|||||||
|
|
||||||
return Content("");
|
return Content("");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{networkId}")]
|
|
||||||
public IActionResult GetTeamAssignments(string networkId, int serverId, string teams = "", bool isDisconnect = false)
|
|
||||||
{
|
|
||||||
return Unauthorized();
|
|
||||||
|
|
||||||
var client = Manager.GetActiveClients()
|
|
||||||
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
|
|
||||||
|
|
||||||
var server = Manager.GetServers().First(c => c.EndPoint == serverId);
|
|
||||||
|
|
||||||
teams = teams ?? string.Empty;
|
|
||||||
|
|
||||||
string assignments = Balance.GetTeamAssignments(client, isDisconnect, server, teams);
|
|
||||||
|
|
||||||
return Content(assignments);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,19 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
|
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.10" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
|
||||||
<ProjectReference Include="..\Stats\Stats.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4ScriptCommands
|
namespace IW4ScriptCommands
|
||||||
{
|
{
|
||||||
class Plugin : IPlugin
|
public class Plugin : IPlugin
|
||||||
{
|
{
|
||||||
public string Name => "IW4 Script Commands";
|
public string Name => "IW4 Script Commands";
|
||||||
|
|
||||||
@ -15,29 +12,29 @@ namespace IW4ScriptCommands
|
|||||||
|
|
||||||
public string Author => "RaidMax";
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
public Task OnEventAsync(GameEvent E, Server S)
|
public async Task OnEventAsync(GameEvent E, Server S)
|
||||||
{
|
{
|
||||||
if (E.Type == GameEvent.EventType.Start)
|
if (E.Type == GameEvent.EventType.Start)
|
||||||
{
|
{
|
||||||
return S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
|
await S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Warn)
|
if (E.Type == GameEvent.EventType.Warn)
|
||||||
{
|
{
|
||||||
return S.SetDvarAsync("sv_iw4madmin_command", new CommandInfo()
|
var cmd = new ScriptCommand()
|
||||||
{
|
{
|
||||||
ClientNumber = E.Target.ClientNumber,
|
ClientNumber = E.Target.ClientNumber,
|
||||||
Command = "alert",
|
CommandName = "alert",
|
||||||
CommandArguments = new List<string>()
|
CommandArguments = new[]
|
||||||
{
|
{
|
||||||
"Warning",
|
"Warning",
|
||||||
"ui_mp_nukebomb_timer",
|
"ui_mp_nukebomb_timer",
|
||||||
E.Data
|
E.Data
|
||||||
}
|
}
|
||||||
}.ToString());
|
};
|
||||||
|
// notifies the player ingame of the warning
|
||||||
|
await cmd.Execute(S);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
|
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
|
||||||
|
36
Plugins/IW4ScriptCommands/ScriptCommand.cs
Normal file
36
Plugins/IW4ScriptCommands/ScriptCommand.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4ScriptCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains basic properties for command information read by gsc
|
||||||
|
/// </summary>
|
||||||
|
class ScriptCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the command to execute
|
||||||
|
/// </summary>
|
||||||
|
public string CommandName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Target client number
|
||||||
|
/// </summary>
|
||||||
|
public int ClientNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Arguments for the script function itself
|
||||||
|
/// </summary>
|
||||||
|
public string[] CommandArguments { get; set; } = new string[0];
|
||||||
|
|
||||||
|
public override string ToString() => string.Join(";", new[] { CommandName, ClientNumber.ToString() }.Concat(CommandArguments).Select(_arg => _arg.Replace(";", "")));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the command
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="server">server to execute the command on</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task Execute(Server server) => await server.SetDvarAsync("sv_iw4madmin_command", ToString());
|
||||||
|
}
|
||||||
|
}
|
391
Plugins/LiveRadar/Configuration/LiveRadarConfiguration.cs
Normal file
391
Plugins/LiveRadar/Configuration/LiveRadarConfiguration.cs
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LiveRadar.Configuration
|
||||||
|
{
|
||||||
|
class LiveRadarConfiguration : IBaseConfiguration
|
||||||
|
{
|
||||||
|
public List<MapInfo> Maps { get; set; }
|
||||||
|
|
||||||
|
public IBaseConfiguration Generate()
|
||||||
|
{
|
||||||
|
Maps = new List<MapInfo>()
|
||||||
|
{
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_afghan",
|
||||||
|
MaxLeft = 4600, // ymax
|
||||||
|
MaxRight = -1100, // ymin
|
||||||
|
MaxBottom = -1400, // xmin
|
||||||
|
MaxTop = 4600, // xmax
|
||||||
|
Left = 52, // pxmin
|
||||||
|
Right = 898, // pxmax
|
||||||
|
Bottom = 930, // pymax
|
||||||
|
Top = 44 // pymin
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_rust",
|
||||||
|
Top = 212,
|
||||||
|
Bottom = 812,
|
||||||
|
Left = 314,
|
||||||
|
Right = 856,
|
||||||
|
MaxRight = -225,
|
||||||
|
MaxLeft = 1809,
|
||||||
|
MaxTop = 1773,
|
||||||
|
MaxBottom = -469
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_terminal",
|
||||||
|
Top = 174,
|
||||||
|
Bottom = 846,
|
||||||
|
Left = 18,
|
||||||
|
Right = 1011,
|
||||||
|
MaxTop = 2929,
|
||||||
|
MaxBottom = -513,
|
||||||
|
MaxLeft = 7521,
|
||||||
|
MaxRight = 2447
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_subbase",
|
||||||
|
MaxLeft = 1841,
|
||||||
|
MaxRight = -3817,
|
||||||
|
MaxBottom = -1585,
|
||||||
|
MaxTop = 2593,
|
||||||
|
Left = 18,
|
||||||
|
Right = 968,
|
||||||
|
Bottom = 864,
|
||||||
|
Top = 160,
|
||||||
|
ViewPositionRotation = 180,
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_estate",
|
||||||
|
Top = 52,
|
||||||
|
Bottom = 999,
|
||||||
|
Left = 173,
|
||||||
|
Right = 942,
|
||||||
|
MaxTop = 2103,
|
||||||
|
MaxBottom = -5077,
|
||||||
|
MaxLeft = 4437,
|
||||||
|
MaxRight = -1240,
|
||||||
|
Rotation = 143,
|
||||||
|
CenterX = -1440,
|
||||||
|
CenterY = 1920,
|
||||||
|
Scaler = 0.85f,
|
||||||
|
ViewPositionRotation = 180
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_highrise",
|
||||||
|
MaxBottom = -3909,
|
||||||
|
MaxTop = 1649,
|
||||||
|
MaxRight = 5111,
|
||||||
|
MaxLeft = 8906,
|
||||||
|
Left = 108,
|
||||||
|
Right = 722,
|
||||||
|
Top = 66,
|
||||||
|
Bottom = 974,
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_quarry",
|
||||||
|
MaxBottom = -5905,
|
||||||
|
MaxTop = -1423,
|
||||||
|
MaxRight = -2095,
|
||||||
|
MaxLeft = 3217,
|
||||||
|
Left = 126,
|
||||||
|
Right = 968,
|
||||||
|
Top = 114,
|
||||||
|
Bottom = 824
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_boneyard",
|
||||||
|
MaxBottom = -1756,
|
||||||
|
MaxTop = 2345,
|
||||||
|
MaxRight = -715,
|
||||||
|
MaxLeft = 1664,
|
||||||
|
Left = 248,
|
||||||
|
Right = 728,
|
||||||
|
Top = 68,
|
||||||
|
Bottom = 897
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_brecourt",
|
||||||
|
MaxBottom = -3797,
|
||||||
|
MaxTop = 4240,
|
||||||
|
MaxRight = -3876,
|
||||||
|
MaxLeft = 2575,
|
||||||
|
Left = 240,
|
||||||
|
Right = 846,
|
||||||
|
Top = 180,
|
||||||
|
Bottom = 934
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_checkpoint",
|
||||||
|
MaxBottom = -2273,
|
||||||
|
MaxTop = 2153,
|
||||||
|
MaxRight = -3457,
|
||||||
|
MaxLeft = 2329,
|
||||||
|
Left = 30,
|
||||||
|
Right = 1010,
|
||||||
|
Top = 136,
|
||||||
|
Bottom = 890
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_derail",
|
||||||
|
MaxBottom = -2775,
|
||||||
|
MaxTop = 3886,
|
||||||
|
MaxRight = -3807,
|
||||||
|
MaxLeft = 4490,
|
||||||
|
Left = 130,
|
||||||
|
Right = 892,
|
||||||
|
Top = 210,
|
||||||
|
Bottom = 829
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_favela",
|
||||||
|
MaxBottom = -2017,
|
||||||
|
MaxTop = 1769,
|
||||||
|
MaxRight = -1239,
|
||||||
|
MaxLeft = 2998,
|
||||||
|
Left = 120,
|
||||||
|
Right = 912,
|
||||||
|
Top = 174,
|
||||||
|
Bottom = 878
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_invasion",
|
||||||
|
MaxBottom = -3673,
|
||||||
|
MaxTop = 2540,
|
||||||
|
MaxRight = -3835,
|
||||||
|
MaxLeft = 980,
|
||||||
|
Left = 20,
|
||||||
|
Right = 808,
|
||||||
|
Top = 0,
|
||||||
|
Bottom = 1006
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_nightshift",
|
||||||
|
MaxBottom = -2497,
|
||||||
|
MaxTop = 1977,
|
||||||
|
MaxRight = -2265,
|
||||||
|
MaxLeft = 945,
|
||||||
|
Left = 246,
|
||||||
|
Right = 826,
|
||||||
|
Top = 104,
|
||||||
|
Bottom = 916
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_rundown",
|
||||||
|
MaxBottom = -2304,
|
||||||
|
MaxTop = 3194,
|
||||||
|
MaxRight = -3558,
|
||||||
|
MaxLeft = 3361,
|
||||||
|
Left = 32,
|
||||||
|
Right = 1030,
|
||||||
|
Top = 96,
|
||||||
|
Bottom = 892
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_underpass",
|
||||||
|
MaxBottom = -601,
|
||||||
|
MaxTop = 3761,
|
||||||
|
MaxRight = -1569,
|
||||||
|
MaxLeft = 3615,
|
||||||
|
Left = 42,
|
||||||
|
Right = 978,
|
||||||
|
Top = 157,
|
||||||
|
Bottom = 944
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_abandon",
|
||||||
|
MaxBottom = -1290,
|
||||||
|
MaxTop = 3855,
|
||||||
|
MaxRight = -2907,
|
||||||
|
MaxLeft = 2723,
|
||||||
|
Left = 6,
|
||||||
|
Right = 1016,
|
||||||
|
Top = 32,
|
||||||
|
Bottom = 945
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_compact",
|
||||||
|
MaxBottom = 0,
|
||||||
|
MaxTop = 4264,
|
||||||
|
MaxRight = -1552,
|
||||||
|
MaxLeft = 3344,
|
||||||
|
Left = 35,
|
||||||
|
Right = 1003,
|
||||||
|
Top = 94,
|
||||||
|
Bottom = 935
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_complex",
|
||||||
|
MaxBottom = -2869,
|
||||||
|
MaxTop = 2867,
|
||||||
|
MaxRight = -4204,
|
||||||
|
MaxLeft = -1218,
|
||||||
|
Left = 282,
|
||||||
|
Right = 749,
|
||||||
|
Top = 48,
|
||||||
|
Bottom = 991
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_crash",
|
||||||
|
MaxBottom = -953,
|
||||||
|
MaxTop = 1811,
|
||||||
|
MaxRight = -2129,
|
||||||
|
MaxLeft = 2277,
|
||||||
|
Left = 52,
|
||||||
|
Right = 1017,
|
||||||
|
Top = 201,
|
||||||
|
Bottom = 807
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_fuel2",
|
||||||
|
MaxBottom = -2218,
|
||||||
|
MaxTop = 4324,
|
||||||
|
MaxRight = -3115,
|
||||||
|
MaxLeft = 3193,
|
||||||
|
Left = 39,
|
||||||
|
Right = 888,
|
||||||
|
Top = 24,
|
||||||
|
Bottom = 906
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_overgrown",
|
||||||
|
MaxBottom = -2052,
|
||||||
|
MaxTop = 3236,
|
||||||
|
MaxRight = -5393,
|
||||||
|
MaxLeft = 808,
|
||||||
|
Left = 17,
|
||||||
|
Right = 1024,
|
||||||
|
Top = 0,
|
||||||
|
Bottom = 847
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_storm",
|
||||||
|
MaxBottom = -2317,
|
||||||
|
MaxTop = 2537,
|
||||||
|
MaxRight = -2223,
|
||||||
|
MaxLeft = 2097,
|
||||||
|
Left = 79,
|
||||||
|
Right = 932,
|
||||||
|
Top = 20,
|
||||||
|
Bottom = 995
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_strike",
|
||||||
|
MaxBottom = -2504,
|
||||||
|
MaxTop = 3359,
|
||||||
|
MaxRight = -3105,
|
||||||
|
MaxLeft = 2822,
|
||||||
|
Left = 40,
|
||||||
|
Right = 969,
|
||||||
|
Top = 36,
|
||||||
|
Bottom = 955
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_trailerpark",
|
||||||
|
MaxBottom = -2709,
|
||||||
|
MaxTop = 2027,
|
||||||
|
MaxRight = -1719,
|
||||||
|
MaxLeft = 1666,
|
||||||
|
Left = 152,
|
||||||
|
Right = 785,
|
||||||
|
Top = 50,
|
||||||
|
Bottom = 931
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_vacant",
|
||||||
|
MaxBottom = -2089,
|
||||||
|
MaxTop = 1652,
|
||||||
|
MaxRight = -1393,
|
||||||
|
MaxLeft = 1789,
|
||||||
|
Left = 122,
|
||||||
|
Right = 909,
|
||||||
|
Top = 16,
|
||||||
|
Bottom = 951
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_nuked",
|
||||||
|
MaxLeft = 1211,
|
||||||
|
MaxRight = -557,
|
||||||
|
MaxBottom = -2110,
|
||||||
|
MaxTop = 2092,
|
||||||
|
Left = 340,
|
||||||
|
Right = 698,
|
||||||
|
Bottom = 930,
|
||||||
|
Top = 92
|
||||||
|
},
|
||||||
|
|
||||||
|
new MapInfo()
|
||||||
|
{
|
||||||
|
Name = "mp_killhouse",
|
||||||
|
MaxLeft = 4276,
|
||||||
|
MaxRight = 2973,
|
||||||
|
MaxBottom = -1164,
|
||||||
|
MaxTop = 1392,
|
||||||
|
Left = 319,
|
||||||
|
Right = 758,
|
||||||
|
Bottom = 937,
|
||||||
|
Top = 87
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name() => "LiveRadar";
|
||||||
|
}
|
||||||
|
}
|
89
Plugins/LiveRadar/Controllers/RadarController.cs
Normal file
89
Plugins/LiveRadar/Controllers/RadarController.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using LiveRadar.Configuration;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace LiveRadar.Web.Controllers
|
||||||
|
{
|
||||||
|
public class RadarController : BaseController
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings()
|
||||||
|
{
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
|
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly IManager _manager;
|
||||||
|
private readonly LiveRadarConfiguration _config;
|
||||||
|
|
||||||
|
public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) : base(manager)
|
||||||
|
{
|
||||||
|
_manager = manager;
|
||||||
|
_config = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration").Configuration() ?? new LiveRadarConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("Radar/{serverId}")]
|
||||||
|
public IActionResult Index(long? serverId = null)
|
||||||
|
{
|
||||||
|
ViewBag.IsFluid = true;
|
||||||
|
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"];
|
||||||
|
ViewBag.ActiveServerId = serverId ?? _manager.GetServers().FirstOrDefault()?.EndPoint;
|
||||||
|
ViewBag.Servers = _manager.GetServers()
|
||||||
|
.Where(_server => _server.GameName == Server.Game.IW4)
|
||||||
|
.Select(_server => new ServerInfo()
|
||||||
|
{
|
||||||
|
Name = _server.Hostname,
|
||||||
|
ID = _server.EndPoint
|
||||||
|
});
|
||||||
|
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("Radar/{serverId}/Map")]
|
||||||
|
public IActionResult Map(long? serverId = null)
|
||||||
|
{
|
||||||
|
var server = serverId == null ? _manager.GetServers().FirstOrDefault() : _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||||
|
var map = _config.Maps.FirstOrDefault(_map => _map.Name == server.CurrentMap.Name);
|
||||||
|
|
||||||
|
if (map != null)
|
||||||
|
{
|
||||||
|
map.Alias = server.CurrentMap.Alias;
|
||||||
|
return Json(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// occurs if we don't recognize the map
|
||||||
|
return StatusCode(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("Radar/{serverId}/Data")]
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public IActionResult Data(long? serverId = null)
|
||||||
|
{
|
||||||
|
var server = serverId == null ? _manager.GetServers()[0] : _manager.GetServers().First(_server => _server.EndPoint == serverId);
|
||||||
|
var radarInfo = server.GetClientsAsList().Select(_client => _client.GetAdditionalProperty<RadarEvent>("LiveRadar")).ToList();
|
||||||
|
return Json(radarInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("Radar/Update")]
|
||||||
|
public IActionResult Update(string payload)
|
||||||
|
{
|
||||||
|
var radarUpdate = RadarEvent.Parse(payload);
|
||||||
|
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||||
|
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
radarUpdate.Name = client.Name.StripColors();
|
||||||
|
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Plugins/LiveRadar/LiveRadar.csproj
Normal file
32
Plugins/LiveRadar/LiveRadar.csproj
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
|
||||||
|
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
|
||||||
|
<PreserveCompilationContext Condition="'$(CONFIG)'!='Debug'">false</PreserveCompilationContext>
|
||||||
|
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||||
|
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||||
|
<Version>0.1.0.0</Version>
|
||||||
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
|
<ApplicationIcon />
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<StartupObject />
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.10" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="Views\_ViewImports.cshtml">
|
||||||
|
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
</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>
|
31
Plugins/LiveRadar/MapInfo.cs
Normal file
31
Plugins/LiveRadar/MapInfo.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace LiveRadar
|
||||||
|
{
|
||||||
|
public class MapInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Alias { get; set; }
|
||||||
|
// distance from the edge of the minimap image
|
||||||
|
// to the "playable" area
|
||||||
|
public int Top { get; set; }
|
||||||
|
public int Bottom { get; set; }
|
||||||
|
public int Left { get; set; }
|
||||||
|
public int Right { get; set; }
|
||||||
|
// maximum coordinate values for the map
|
||||||
|
public int MaxTop { get; set; }
|
||||||
|
public int MaxBottom { get; set; }
|
||||||
|
public int MaxLeft { get; set; }
|
||||||
|
public int MaxRight { get; set; }
|
||||||
|
public float Rotation { get; set; }
|
||||||
|
public float ViewPositionRotation { get; set; }
|
||||||
|
public float CenterX { get; set; }
|
||||||
|
public float CenterY { get; set; }
|
||||||
|
public float Scaler { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
public int Width => MaxLeft - MaxRight;
|
||||||
|
public int Height => MaxTop - MaxBottom;
|
||||||
|
}
|
||||||
|
}
|
90
Plugins/LiveRadar/Plugin.cs
Normal file
90
Plugins/LiveRadar/Plugin.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using LiveRadar.Configuration;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LiveRadar
|
||||||
|
{
|
||||||
|
public class Plugin : IPlugin
|
||||||
|
{
|
||||||
|
public string Name => "Live Radar";
|
||||||
|
|
||||||
|
public float Version => (float)Utilities.GetVersionAsDouble();
|
||||||
|
|
||||||
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
|
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
|
||||||
|
private bool addedPage;
|
||||||
|
private readonly object lockObject = new object();
|
||||||
|
|
||||||
|
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
|
||||||
|
{
|
||||||
|
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
|
{
|
||||||
|
// if it's an IW4 game, with custom callbacks, we want to
|
||||||
|
// enable the live radar page
|
||||||
|
lock (lockObject)
|
||||||
|
{
|
||||||
|
if (E.Type == GameEvent.EventType.Start &&
|
||||||
|
S.GameName == Server.Game.IW4 &&
|
||||||
|
S.CustomCallback &&
|
||||||
|
!addedPage)
|
||||||
|
{
|
||||||
|
E.Owner.Manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All");
|
||||||
|
addedPage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (E.Type == GameEvent.EventType.Unknown)
|
||||||
|
{
|
||||||
|
if (E.Data?.StartsWith("LiveRadar") ?? false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var radarUpdate = RadarEvent.Parse(E.Data);
|
||||||
|
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||||
|
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
radarUpdate.Name = client.Name.StripColors();
|
||||||
|
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
|
||||||
|
S.Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnLoadAsync(IManager manager)
|
||||||
|
{
|
||||||
|
if (_configurationHandler.Configuration() == null)
|
||||||
|
{
|
||||||
|
_configurationHandler.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate());
|
||||||
|
await _configurationHandler.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnTickAsync(Server S)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnUnloadAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
Plugins/LiveRadar/RadarEvent.cs
Normal file
64
Plugins/LiveRadar/RadarEvent.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace LiveRadar
|
||||||
|
{
|
||||||
|
public class RadarEvent
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public long Guid { get; set; }
|
||||||
|
public Vector3 Location { get; set; }
|
||||||
|
public Vector3 ViewAngles { get; set; }
|
||||||
|
public string Team { get; set; }
|
||||||
|
public int Kills { get; set; }
|
||||||
|
public int Deaths { get; set; }
|
||||||
|
public int Score { get; set; }
|
||||||
|
public int PlayTime { get; set; }
|
||||||
|
public string Weapon { get; set; }
|
||||||
|
public int Health { get; set; }
|
||||||
|
public bool IsAlive { get; set; }
|
||||||
|
public Vector3 RadianAngles => new Vector3(ViewAngles.X.ToRadians(), ViewAngles.Y.ToRadians(), ViewAngles.Z.ToRadians());
|
||||||
|
public int Id => GetHashCode();
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj is RadarEvent re)
|
||||||
|
{
|
||||||
|
return re.ViewAngles.X == ViewAngles.X &&
|
||||||
|
re.ViewAngles.Y == ViewAngles.Y &&
|
||||||
|
re.ViewAngles.Z == ViewAngles.Z &&
|
||||||
|
re.Location.X == Location.X &&
|
||||||
|
re.Location.Y == Location.Y &&
|
||||||
|
re.Location.Z == Location.Z;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RadarEvent Parse(string input)
|
||||||
|
{
|
||||||
|
var items = input.Split(';').Skip(1).ToList();
|
||||||
|
|
||||||
|
var parsedEvent = new RadarEvent()
|
||||||
|
{
|
||||||
|
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber),
|
||||||
|
Location = Vector3.Parse(items[1]),
|
||||||
|
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
|
||||||
|
Team = items[3],
|
||||||
|
Kills = int.Parse(items[4]),
|
||||||
|
Deaths = int.Parse(items[5]),
|
||||||
|
Score = int.Parse(items[6]),
|
||||||
|
Weapon = items[7],
|
||||||
|
Health = int.Parse(items[8]),
|
||||||
|
IsAlive = items[9] == "1",
|
||||||
|
PlayTime = Convert.ToInt32(items[10])
|
||||||
|
};
|
||||||
|
|
||||||
|
return parsedEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
470
Plugins/LiveRadar/Views/Radar/Index.cshtml
Normal file
470
Plugins/LiveRadar/Views/Radar/Index.cshtml
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
@model IEnumerable<long>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.progress {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-stat-icon {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
background-size: 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="row p-0 ml-auto mr-auto mb-4">
|
||||||
|
<div class="col-12 col-xl-10 p-0 ml-auto mr-auto p-0 pl-lg-3 pr-lg-3 ">
|
||||||
|
<ul class="nav nav-tabs border-top border-bottom nav-fill" role="tablist">
|
||||||
|
@foreach (SharedLibraryCore.Dtos.ServerInfo server in ViewBag.Servers)
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a asp-controller="Radar" asp-action="Index" asp-route-serverId="@server.ID" class="nav-link @(server.ID == ViewBag.ActiveServerId ? "active": "")" aria-selected="@(server.ID == ViewBag.ActiveServerId ? "true": "false")"><color-code value="@server.Name" allow="@ViewBag.EnableColorCodes"></color-code></a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row p-0 ml-auto mr-auto col-12 col-xl-10">
|
||||||
|
<div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-right text-center player-data-left" style="opacity: 0;">
|
||||||
|
</div>
|
||||||
|
<div class="pl-0 pr-0 pl-lg-3 pr-lg-3 col-lg-6 col-12 pb-4">
|
||||||
|
<div id="map_name" class="h4 text-center pb-2 pt-2 mb-0 bg-primary">—</div>
|
||||||
|
<div id="map_list" style="background-size:cover; padding-bottom: 100% !important;">
|
||||||
|
<canvas id="map_canvas" style="position:absolute;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-left text-center player-data-right" style="opacity: 0;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- images used by canvas -->
|
||||||
|
<img class="hide" id="hud_death" src="~/images/radar/death.png" />
|
||||||
|
|
||||||
|
|
||||||
|
@section scripts {
|
||||||
|
|
||||||
|
<script defer="defer">
|
||||||
|
const textOffset = 15;
|
||||||
|
let previousRadarData = undefined;
|
||||||
|
let newRadarData = undefined;
|
||||||
|
|
||||||
|
/************************
|
||||||
|
* IW4 *
|
||||||
|
* **********************/
|
||||||
|
const weapons = {};
|
||||||
|
weapons["ak47"] = "ak47";
|
||||||
|
weapons["ak47classic"] = "icon_ak47_classic";
|
||||||
|
weapons["ak74u"] = "akd74u";
|
||||||
|
weapons["m16"] = "m16a4";
|
||||||
|
weapons["m4"] = "m4carbine";
|
||||||
|
weapons["fn2000"] = "fn2000";
|
||||||
|
weapons["masada"] = "masada";
|
||||||
|
weapons["famas"] = "famas";
|
||||||
|
weapons["fal"] = "fnfal";
|
||||||
|
weapons["scar"] = "scar_h";
|
||||||
|
weapons["tavor"] = "tavor";
|
||||||
|
|
||||||
|
weapons["mp5k"] = "mp5k";
|
||||||
|
weapons["uzi"] = "mini_uzi";
|
||||||
|
weapons["p90"] = "p90";
|
||||||
|
weapons["kriss"] = "kriss";
|
||||||
|
weapons["ump45"] = "ump45";
|
||||||
|
|
||||||
|
weapons["rpd"] = "rpd";
|
||||||
|
weapons["sa80"] = "sa80_lmg";
|
||||||
|
weapons["mg4"] = "mg4";
|
||||||
|
weapons["m240"] = "m240";
|
||||||
|
weapons["aug"] = "steyr";
|
||||||
|
|
||||||
|
weapons["barrett"] = "barrett50cal";
|
||||||
|
weapons["wa2000"] = "wa2000";
|
||||||
|
weapons["m21"] = "m14ebr";
|
||||||
|
weapons["cheytac"] = "cheytac";
|
||||||
|
weapons["dragunov"] = "hud_dragunovsvd";
|
||||||
|
|
||||||
|
weapons["beretta"] = "m9beretta";
|
||||||
|
weapons["usp"] = "usp_45";
|
||||||
|
weapons["deserteagle"] = "desert_eagle";
|
||||||
|
weapons["deserteaglegold"] = "desert_eagle_gold";
|
||||||
|
weapons["desert"]
|
||||||
|
weapons["coltanaconda"] = "colt_anaconda";
|
||||||
|
|
||||||
|
weapons["tmp"] = "mp9";
|
||||||
|
weapons["glock"] = "glock";
|
||||||
|
weapons["beretta393"] = "beretta393";
|
||||||
|
weapons["pp2000"] = "pp2000";
|
||||||
|
|
||||||
|
weapons["ranger"] = "sawed_off";
|
||||||
|
weapons["model1887"] = "model1887";
|
||||||
|
weapons["striker"] = "striker";
|
||||||
|
weapons["aa12"] = "aa12";
|
||||||
|
weapons["m1014"] = "benelli_m4";
|
||||||
|
weapons["spas12"] = "spas12";
|
||||||
|
|
||||||
|
weapons["m79"] = "m79";
|
||||||
|
weapons["rpg"] = "rpg";
|
||||||
|
weapons["at4"] = "at4";
|
||||||
|
weapons["stinger"] = "stinger";
|
||||||
|
weapons["javelin"] = "javelin";
|
||||||
|
|
||||||
|
weapons["m40a3"] = "m40a3";
|
||||||
|
weapons["none"] = "neutral";
|
||||||
|
weapons["riotshield"] = "riot_shield";
|
||||||
|
weapons["peacekeeper"] = "peacekeeper";
|
||||||
|
|
||||||
|
function drawCircle(context, x, y, color) {
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(x, y, 6 * stateInfo.imageScaler, 0, 2 * Math.PI, false);
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fill();
|
||||||
|
context.lineWidth = 0.5;
|
||||||
|
context.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
||||||
|
context.closePath();
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLine(context, x1, y1, x2, y2, color) {
|
||||||
|
context.beginPath();
|
||||||
|
context.lineWidth = '3';
|
||||||
|
context.moveTo(x1, y1);
|
||||||
|
context.lineTo(x2, y2);
|
||||||
|
context.closePath();
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTriangle(context, v1, v2, v3, color) {
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(v1.x, v1.y);
|
||||||
|
context.lineTo(v2.x, v2.y);
|
||||||
|
context.lineTo(v3.x, v3.y);
|
||||||
|
context.closePath();
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawText(context, x, y, text, size, fillColor, strokeColor, alignment = 'left') {
|
||||||
|
context.beginPath();
|
||||||
|
context.save();
|
||||||
|
context.font = `bold ${Math.max(12, size * stateInfo.imageScaler)}px courier new`;
|
||||||
|
context.fillStyle = fillColor;
|
||||||
|
context.shadowColor = strokeColor;
|
||||||
|
context.shadowBlur = 4;
|
||||||
|
context.textAlign = alignment;
|
||||||
|
context.fillText(text, x, y);
|
||||||
|
context.restore();
|
||||||
|
context.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawImage(context, imgSelector, x, y, alpha = 1) {
|
||||||
|
context.save();
|
||||||
|
context.globalAlpha = alpha;
|
||||||
|
context.drawImage(document.getElementById(imgSelector), x - (15 * stateInfo.imageScaler), y - (15 * stateInfo.imageScaler), 32 * stateInfo.imageScaler, 32 * stateInfo.imageScaler);
|
||||||
|
context.globalAlpha = 1;
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkCanvasSize(canvas, context, minimap, map) {
|
||||||
|
|
||||||
|
let width = Math.round(minimap.width());
|
||||||
|
if (Math.round(context.canvas.width) != width) {
|
||||||
|
|
||||||
|
canvas.width(width);
|
||||||
|
canvas.height(width);
|
||||||
|
|
||||||
|
context.canvas.height = width;
|
||||||
|
context.canvas.width = context.canvas.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateInfo.imageScaler = (stateInfo.canvas.width() / 1024)
|
||||||
|
stateInfo.mapScalerX = (((stateInfo.mapInfo.right * stateInfo.imageScaler) - (stateInfo.mapInfo.left * stateInfo.imageScaler)) / stateInfo.mapInfo.width);
|
||||||
|
stateInfo.mapScalerY = (((stateInfo.mapInfo.bottom * stateInfo.imageScaler) - (stateInfo.mapInfo.top * stateInfo.imageScaler)) / stateInfo.mapInfo.height);
|
||||||
|
stateInfo.mapScaler = (stateInfo.mapScalerX + stateInfo.mapScalerY) / 2
|
||||||
|
|
||||||
|
stateInfo.forwardDistance = 500.0;
|
||||||
|
stateInfo.fovWidth = 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateViewPosition(x, y, distance) {
|
||||||
|
let nx = Math.cos(x) * Math.cos(y);
|
||||||
|
let ny = Math.sin(x) * Math.cos(y);
|
||||||
|
let nz = Math.sin(360.0 - y);
|
||||||
|
|
||||||
|
return { x: (nx * distance) * stateInfo.mapScaler, y: (ny * distance) * stateInfo.mapScaler, z: (nz * distance) * stateInfo.mapScaler };
|
||||||
|
}
|
||||||
|
|
||||||
|
function lerp(start, end, complete) {
|
||||||
|
return (1 - complete) * start + complete * end;
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeLerp(start, end, t) {
|
||||||
|
let t2 = (1 - Math.cos(t * Math.PI)) / 2;
|
||||||
|
|
||||||
|
return (start * (1-t2) + end * t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixRollAngles(oldAngles, newAngles) {
|
||||||
|
let newX = newAngles.x;
|
||||||
|
let newY = newAngles.y;
|
||||||
|
|
||||||
|
let angleDifferenceX = (oldAngles.x - newAngles.x);
|
||||||
|
|
||||||
|
if (angleDifferenceX > Math.PI) {
|
||||||
|
newX = oldAngles.x + (Math.PI * 2) - angleDifferenceX;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (Math.abs(newAngles.x - oldAngles.x) > Math.PI) {
|
||||||
|
newX = newAngles.x - (Math.PI * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let angleDifferenceY = (oldAngles.y - newAngles.y);
|
||||||
|
|
||||||
|
if (angleDifferenceY > Math.PI) {
|
||||||
|
newY = oldAngles.y + (Math.PI * 2) - angleDifferenceY;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (Math.abs(newAngles.y - oldAngles.y) > Math.PI) {
|
||||||
|
newY = newAngles.y - (Math.PI * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: newX, y: newY };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRadians(deg) {
|
||||||
|
return deg * Math.PI / 180.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotate(cx, cy, x, y, angle) {
|
||||||
|
var radians = (Math.PI / 180) * angle,
|
||||||
|
cos = Math.cos(radians),
|
||||||
|
sin = Math.sin(radians),
|
||||||
|
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
|
||||||
|
ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
|
||||||
|
return {
|
||||||
|
x: nx,
|
||||||
|
y: ny
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function weaponImageForWeapon(weapon) {
|
||||||
|
let name = weapon.split('_')[0];
|
||||||
|
if (weapons[name] == undefined) {
|
||||||
|
console.log(name);
|
||||||
|
name = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `../images/radar/hud_weapons/hud_${weapons[name]}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlayerData() {
|
||||||
|
$('.player-data-left').html('');
|
||||||
|
$('.player-data-right').html('');
|
||||||
|
|
||||||
|
$.each(newRadarData, function (index, player) {
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = index % 2 == 0 ? $('.player-data-left') : $('.player-data-right');
|
||||||
|
column.append(`<div class="progress" style="height: 1.5rem; background-color: transparent;">
|
||||||
|
<div style="position: absolute; font-size: 1rem; left: 1.5rem;">${player.name}</div>
|
||||||
|
<div class="progress-bar bg-success" role="progressbar" style="min-width: 0px; width: ${player.health}%" aria-valuenow="${player.health}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
<div class="progress-bar bg-danger" role="progressbar" style="min-width: 0px; border-right: 0px; width: ${100 - player.health}%" aria-valuenow="${100 - player.health}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row flex-wrap p-2 mb-4 bg-dark border-bottom">
|
||||||
|
<div style="width: 3rem; height: 1.5rem; background-image:url(${weaponImageForWeapon(player.weapon)}); background-size: 3rem 1.5rem;" class="mr-auto text-left">
|
||||||
|
</div>
|
||||||
|
<div class="player-stat-icon" style="background-image:url('/images/radar/kills.png')"></div>
|
||||||
|
<div class="pr-2">${player.kills}</div>
|
||||||
|
<div class="player-stat-icon" style="background-image:url('/images/radar/death.png')"></div>
|
||||||
|
<div class="pr-3">${player.deaths}</div>
|
||||||
|
<span class="align-self-center oi oi-target pr-1"></span>
|
||||||
|
<div class="pr-3 ">${player.deaths == 0 ? player.kills.toFixed(2) : (player.kills / player.deaths).toFixed(2)}</div>
|
||||||
|
<span class="align-self-center oi oi-graph pr-1"></span>
|
||||||
|
<div>${ player.playTime == 0 ? '—' : Math.round(player.score / (player.playTime / 60))}</div>
|
||||||
|
</div>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.player-data-left').delay(1000).animate({opacity: 1}, 500);
|
||||||
|
$('.player-data-right').delay(1000).animate({opacity: 1}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateInfo = {
|
||||||
|
canvas: $('#map_canvas'),
|
||||||
|
ctx: $('#map_canvas')[0].getContext('2d'),
|
||||||
|
updateFrequency: 750,
|
||||||
|
updateFrameTimeDeviation: 0,
|
||||||
|
forwardDistance: undefined,
|
||||||
|
fovWidth: undefined,
|
||||||
|
mapInfo: undefined,
|
||||||
|
mapScaler: undefined,
|
||||||
|
deathIcons: {},
|
||||||
|
deathIconTime: 4000
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateRadarData() {
|
||||||
|
$.getJSON('@Url.Action("Data", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_radarItem) {
|
||||||
|
newRadarData = _radarItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$.getJSON('@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_map) {
|
||||||
|
stateInfo.mapInfo = _map
|
||||||
|
});
|
||||||
|
|
||||||
|
$.each(newRadarData, function (index, value) {
|
||||||
|
if (previousRadarData != undefined && index < previousRadarData.length) {
|
||||||
|
|
||||||
|
let previous = previousRadarData[index];
|
||||||
|
|
||||||
|
// this happens when the player has first joined and we haven't gotten two snapshots yet
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous == null) {
|
||||||
|
previous = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't want to treat a disconnected player snapshot as the previous
|
||||||
|
else if (previous.guid == value.guid) {
|
||||||
|
value.previous = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we haven't gotten a new item, it's just the old one again
|
||||||
|
if (previous.id === value.id) {
|
||||||
|
value.animationTime = previous.animationTime;
|
||||||
|
value.previous = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// they died between this snapshot and last so we wanna setup the death icon
|
||||||
|
if (!value.isAlive && previous.isAlive) {
|
||||||
|
stateInfo.deathIcons[value.guid] = {
|
||||||
|
animationTime: now,
|
||||||
|
location: value.location
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// they respawned between this snapshot and last so we don't want to show wherever the were specating from
|
||||||
|
else if (value.isAlive && !previous.isAlive) {
|
||||||
|
value.previous = value;
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
// we switch out the items to
|
||||||
|
previousRadarData = newRadarData;
|
||||||
|
|
||||||
|
$('#map_name').html(stateInfo.mapInfo.alias);
|
||||||
|
$('#map_list').css('background-image', `url(../images/radar/minimaps/compass_map_${stateInfo.mapInfo.name}@('@')2x.jpg)`);
|
||||||
|
checkCanvasSize(stateInfo.canvas, stateInfo.ctx, $('#map_list'), stateInfo.mapInfo);
|
||||||
|
updatePlayerData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMap() {
|
||||||
|
let ctx = stateInfo.ctx;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
now = performance.now();
|
||||||
|
|
||||||
|
$.each(previousRadarData, function (index, value) {
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.previous == null) {
|
||||||
|
value.previous = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this indicates we got a new snapshot to work with so we set the time based off the previous
|
||||||
|
// frame deviation to have minimal interpolation skipping
|
||||||
|
if (value.animationTime === undefined) {
|
||||||
|
value.animationTime = now - stateInfo.updateFrameTimeDeviation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.isAlive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedFrameTime = now - value.animationTime;
|
||||||
|
const completionPercent = elapsedFrameTime / stateInfo.updateFrequency;
|
||||||
|
|
||||||
|
// certain maps like estate have an off center axis of origin, so we need to account for that
|
||||||
|
let rotatedPreviousLocation = rotate(stateInfo.mapInfo.centerX, stateInfo.mapInfo.centerY, value.previous.location.x, value.previous.location.y, stateInfo.mapInfo.rotation);
|
||||||
|
let rotatedCurrentLocation = rotate(stateInfo.mapInfo.centerX, stateInfo.mapInfo.centerY, value.location.x, value.location.y, stateInfo.mapInfo.rotation);
|
||||||
|
|
||||||
|
const startX = ((stateInfo.mapInfo.maxLeft - rotatedPreviousLocation.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
|
||||||
|
const startY = ((stateInfo.mapInfo.maxTop - rotatedPreviousLocation.x) * stateInfo.mapScalerY) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
|
||||||
|
|
||||||
|
const endX = ((stateInfo.mapInfo.maxLeft - rotatedCurrentLocation.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
|
||||||
|
const endY = ((stateInfo.mapInfo.maxTop - rotatedCurrentLocation.x) * stateInfo.mapScalerY) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
|
||||||
|
|
||||||
|
let teamColor = value.team == 'allies' ? 'rgb(0, 122, 204, 1)' : 'rgb(255, 69, 69)';
|
||||||
|
let fovColor = value.team == 'allies' ? 'rgba(0, 122, 204, 0.2)' : 'rgba(255, 69, 69, 0.2)';
|
||||||
|
|
||||||
|
// this takes care of moving past the roll-over point of yaw/pitch (ie 360->0)
|
||||||
|
const rollAngleFix = fixRollAngles(value.previous.radianAngles, value.radianAngles);
|
||||||
|
|
||||||
|
const radianLerpX = lerp(value.previous.radianAngles.x, rollAngleFix.x, completionPercent);
|
||||||
|
const radianLerpY = lerp(value.previous.radianAngles.y, rollAngleFix.y, completionPercent);
|
||||||
|
|
||||||
|
// this is some jankiness to get the fov to point the right direction
|
||||||
|
let firstVertex = calculateViewPosition(toRadians(stateInfo.mapInfo.rotation + stateInfo.mapInfo.viewPositionRotation - 90) - radianLerpX + toRadians(stateInfo.fovWidth), radianLerpY, stateInfo.forwardDistance);
|
||||||
|
let secondVertex = calculateViewPosition(toRadians(stateInfo.mapInfo.rotation + stateInfo.mapInfo.viewPositionRotation - 90) - radianLerpX - toRadians(stateInfo.fovWidth), radianLerpY, stateInfo.forwardDistance);
|
||||||
|
|
||||||
|
let currentX = lerp(startX, endX, completionPercent);
|
||||||
|
let currentY = lerp(startY, endY, completionPercent);
|
||||||
|
|
||||||
|
// we need to calculate the distance from the center of the map so we can scale if necessary
|
||||||
|
let centerX = ((stateInfo.mapInfo.maxLeft - stateInfo.mapInfo.centerY) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
|
||||||
|
let centerY = ((stateInfo.mapInfo.maxTop - stateInfo.mapInfo.centerX) * stateInfo.mapScaler) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
|
||||||
|
|
||||||
|
// reuse lerp to scale the pixel to map ratio
|
||||||
|
currentX = lerp(centerX, currentX, stateInfo.mapInfo.scaler);
|
||||||
|
currentY = lerp(centerY, currentY, stateInfo.mapInfo.scaler);
|
||||||
|
|
||||||
|
drawCircle(ctx, currentX, currentY, teamColor);
|
||||||
|
drawTriangle(ctx,
|
||||||
|
{ x: currentX, y: currentY },
|
||||||
|
{ x: currentX + firstVertex.x, y: currentY + firstVertex.y },
|
||||||
|
{ x: currentX + secondVertex.x, y: currentY + secondVertex.y },
|
||||||
|
fovColor);
|
||||||
|
drawText(ctx, currentX, currentY - (textOffset * stateInfo.imageScaler), value.name, 16, 'white', teamColor, 'center')
|
||||||
|
});
|
||||||
|
|
||||||
|
const completedIcons = [];
|
||||||
|
|
||||||
|
for (let key in stateInfo.deathIcons) {
|
||||||
|
const icon = stateInfo.deathIcons[key];
|
||||||
|
|
||||||
|
const x = ((stateInfo.mapInfo.maxLeft - icon.location.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
|
||||||
|
const y = ((stateInfo.mapInfo.maxTop - icon.location.x) * stateInfo.mapScaler) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
|
||||||
|
|
||||||
|
const elapsedFrameTime = now - icon.animationTime;
|
||||||
|
const completionPercent = elapsedFrameTime / stateInfo.deathIconTime;
|
||||||
|
const opacity = easeLerp(1, 0, completionPercent);
|
||||||
|
|
||||||
|
drawImage(stateInfo.ctx, 'hud_death', x, y, opacity);
|
||||||
|
|
||||||
|
if (completionPercent >= 1) {
|
||||||
|
completedIcons.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < completedIcons.length; i++) {
|
||||||
|
delete stateInfo.deathIcons[completedIcons[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(updateMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.getJSON('@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_map) {
|
||||||
|
stateInfo.mapInfo = _map;
|
||||||
|
updateRadarData();
|
||||||
|
setInterval(updateRadarData, stateInfo.updateFrequency);
|
||||||
|
window.requestAnimationFrame(updateMap);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
}
|
3
Plugins/LiveRadar/Views/_ViewImports.cshtml
Normal file
3
Plugins/LiveRadar/Views/_ViewImports.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@using SharedLibraryCore
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@addTagHelper *, SharedLibraryCore
|
BIN
Plugins/LiveRadar/wwwroot/images/radar/death.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/death.png
Normal file
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_aa12.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_aa12.png
Normal file
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_ak47.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_ak47.png
Normal file
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_at4.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_at4.png
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_famas.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_famas.png
Normal file
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_fnfal.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_fnfal.png
Normal file
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_glock.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_glock.png
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_kriss.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_kriss.png
Normal file
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m16a4.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m16a4.png
Normal file
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m240.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m240.png
Normal file
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m40a3.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m40a3.png
Normal file
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m79.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_m79.png
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_mg4.png
Normal file
BIN
Plugins/LiveRadar/wwwroot/images/radar/hud_weapons/hud_mg4.png
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user