Compare commits
354 Commits
2022.04.20
...
release/pr
Author | SHA1 | Date | |
---|---|---|---|
95eb73da6e | |||
6ec0a24ca2 | |||
03b5b8b143 | |||
005a8b050d | |||
2c99f7b48e | |||
13d4ec3033 | |||
e6cdae5a6b | |||
d69a9ecf56 | |||
b6c32181b0 | |||
2017eebeba | |||
3192fe35e6 | |||
c1dace4af6 | |||
5c6ae3146a | |||
2e99db2275 | |||
79eec08590 | |||
69691f75f4 | |||
648eec25f2 | |||
80774853b6 | |||
08edbf9bd4 | |||
e472468c02 | |||
d4e266ed94 | |||
4e02e7841f | |||
dc707f75b3 | |||
41efe26a48 | |||
4740479ace | |||
6f28bc5b0b | |||
47ed505fae | |||
e2c07daece | |||
28fd712a63 | |||
3f11a4fe9f | |||
bcb063730c | |||
789981346a | |||
f79ba6466c | |||
871f8d75df | |||
ad89ecb39d | |||
2340e30c2d | |||
e7f5e6a841 | |||
50593f5a93 | |||
5a22a759a8 | |||
eb8ea5e222 | |||
3f0bdfe3a9 | |||
2fcbab9a37 | |||
e843f839f5 | |||
e4535e09a0 | |||
b4f93602ef | |||
bc34211e43 | |||
7323c6e3d7 | |||
ebdad2768d | |||
58e8d54373 | |||
3f71bc96f4 | |||
84ed9c8d8f | |||
81e2a2f6d4 | |||
088f7a51be | |||
7d436ac0c5 | |||
c26489d71f | |||
7f4eb230be | |||
003945c241 | |||
ba911f26ec | |||
d6d2717771 | |||
35f9eb5933 | |||
4233aab1ee | |||
cdf9485903 | |||
108dddb5cc | |||
399e082b61 | |||
35c4bbd2d5 | |||
cae77357ca | |||
f186e3ae4d | |||
1e88f5bac0 | |||
ce054c173e | |||
740df7c3ee | |||
466ae96874 | |||
6ae15261c9 | |||
72df5c9902 | |||
994dbe142e | |||
ed3f9f750f | |||
9b56ff520f | |||
123d84088f | |||
ddfcf6e138 | |||
92992dfb13 | |||
c53e0de7d0 | |||
29d0686f73 | |||
caddc06c70 | |||
75b93bb972 | |||
b022b08bc7 | |||
bb8f3fbe5b | |||
c3be7f7de5 | |||
520a76a15e | |||
e8ab56cd9b | |||
5490d6b358 | |||
5d53c2559b | |||
22af762a9d | |||
c550d424dd | |||
f4ded4cc1f | |||
d8c0cd47f5 | |||
1f77d10eed | |||
222f2ba5f8 | |||
8c48151ab6 | |||
c5a283a02e | |||
d0911b7b8a | |||
388434133b | |||
6bb97c7d83 | |||
c348283c94 | |||
a434420951 | |||
19bbdede45 | |||
129e70c82c | |||
c6c7ca6305 | |||
12ddb87fc2 | |||
bc0ec6c050 | |||
99e0990770 | |||
af2925287d | |||
ffb32ccc45 | |||
e558d912cf | |||
2e6a1efb47 | |||
4442826bcf | |||
6db1f6db07 | |||
d9d5a56ab0 | |||
f41ce39180 | |||
2e726ea9ed | |||
6fa172d757 | |||
da54c5d327 | |||
fb82cbe6f2 | |||
5f5c0f1cfb | |||
5f5fb8230e | |||
51fae05a73 | |||
c14042a109 | |||
fab3cf95d6 | |||
ad20572879 | |||
3364473ce2 | |||
710382d432 | |||
b258d51863 | |||
782201b086 | |||
676589a3e0 | |||
6c9ac1f7bb | |||
e8bdde70fb | |||
dab429776d | |||
5e32536821 | |||
59e3813fa7 | |||
66c0561e7f | |||
7b8f6421aa | |||
4ba56b53a4 | |||
a50e61318c | |||
83207b4b40 | |||
ba9e393363 | |||
2688790736 | |||
8fc47ec6c4 | |||
12e3fd9238 | |||
6edf3f1ae9 | |||
8f20a2e2cd | |||
b002991686 | |||
ba40478d11 | |||
c89314667c | |||
b14d7b6865 | |||
dabad54872 | |||
74e792bfdc | |||
eac8483885 | |||
0ebd582532 | |||
31e3e98d06 | |||
9ef189d303 | |||
ae101f1c3a | |||
ef5e36b224 | |||
d0f72390fb | |||
8bcc9354fd | |||
0674ef800b | |||
1349cf84b7 | |||
b311ecefc2 | |||
16739ce455 | |||
b5b01cba4c | |||
797642f3e6 | |||
6fa15d3dcc | |||
7d6bf88bfd | |||
2e149ddafd | |||
a16986f7a3 | |||
dbca3675ba | |||
973ea83ab9 | |||
69cb4bf9df | |||
9a08997825 | |||
9cf91d030d | |||
c06b0982a7 | |||
f4e7d5daf9 | |||
f6b3eb04f2 | |||
565f22b42e | |||
7c1c2e719b | |||
f50d067c73 | |||
a3fa5212f5 | |||
12357fd9f7 | |||
3367c5c22f | |||
3295315339 | |||
cf51b83cdd | |||
76925a78d4 | |||
7b869a3f43 | |||
0ce9dec3ea | |||
069e6a0517 | |||
778feb8024 | |||
44f22dae3a | |||
cf3209e1d0 | |||
a15da15d3e | |||
3b83729457 | |||
407ce2bc8f | |||
24d91f228b | |||
53cbd11008 | |||
186db53bad | |||
40466f84c4 | |||
bdb5a1c5f8 | |||
5d9e2b3bf1 | |||
1cf99869f6 | |||
12da0f463b | |||
e88071684d | |||
cd6097d133 | |||
d5cf4451a2 | |||
1e1e8bbe7b | |||
dadd236069 | |||
2380f23dbe | |||
3cffdfdd9d | |||
400c5d1f4d | |||
ca35fbb19f | |||
809cb0b7f4 | |||
18f23fd07d | |||
7526f86dab | |||
527ffbaced | |||
6f086ac565 | |||
cf4dd6a868 | |||
3efafa24ff | |||
fe919251fb | |||
a67f7f9351 | |||
e99ca3c140 | |||
ccedb01e8d | |||
841bcf6156 | |||
b381af5fba | |||
444c06e65e | |||
561909158f | |||
cd12c3f26e | |||
c817f9a810 | |||
b27ae1517e | |||
507688a175 | |||
d2cfd50e39 | |||
51e8b31e42 | |||
fa1567d3f5 | |||
f97e266c24 | |||
506b17dbb3 | |||
bef8c08d90 | |||
b78c467539 | |||
c3e042521a | |||
cb5f490d3b | |||
0a55c54c42 | |||
f43f7b5040 | |||
540cf7489d | |||
1a72faee60 | |||
4e44bb5ea1 | |||
9e17bcc38f | |||
4b33b33d01 | |||
6f1bc7ab90 | |||
63e1774cb6 | |||
61df873bb1 | |||
052eeb0615 | |||
88e67747fe | |||
5db94723aa | |||
ea8216ecdf | |||
6abbcbe464 | |||
57484690b6 | |||
7a022a1973 | |||
7108e23a03 | |||
77d25890da | |||
2fca68a7ea | |||
a6c0a94f6c | |||
71abaac9e1 | |||
e07651b931 | |||
5a2ee36df9 | |||
2daa4991d1 | |||
775c0a91b5 | |||
55bccc7d3d | |||
4322e8d882 | |||
a92f9fc29c | |||
fbf424c77d | |||
b8e001fcfe | |||
5ab5b73ecf | |||
4534d24fe6 | |||
73c8d0da33 | |||
16d75470b5 | |||
f02552faa1 | |||
a4923d03f9 | |||
8ae6561f4e | |||
deeb1dea87 | |||
9ab34614c5 | |||
2cff25d6b3 | |||
df3e226dc9 | |||
ef3db63ba7 | |||
49fe4520ff | |||
6587187a34 | |||
b337e232a2 | |||
a44b4e9475 | |||
ffb0e5cac1 | |||
ecc2b5bf54 | |||
2ac9cc4379 | |||
215037095f | |||
5433d7d1d2 | |||
0446fe1ec5 | |||
cf2a00e5b3 | |||
ab494a22cb | |||
b690579154 | |||
acc967e50a | |||
c493fbe13d | |||
ee56a5db1f | |||
f235d0fafd | |||
7ecf516278 | |||
210f1ca336 | |||
a38789adb9 | |||
e459b2fcde | |||
26853a0005 | |||
ee14306db9 | |||
169105e849 | |||
7c10e0e3de | |||
2f7eb07e39 | |||
0f9f4f597b | |||
880f9333d9 | |||
31da5d352e | |||
83a469cae3 | |||
1f13f9122c | |||
dd8c4f438f | |||
2230036d45 | |||
1700b7da91 | |||
fab97ccad4 | |||
0bf0d033f7 | |||
2bbabcb9e8 | |||
1995dbd080 | |||
a3b94b50e3 | |||
bc38b36e4a | |||
e346aa037e | |||
389c687420 | |||
074e36413e | |||
104d9bdc4c | |||
c51d28937b | |||
ffa8a46feb | |||
91c46dbdd4 | |||
ff0d22c142 | |||
3ad4aa2196 | |||
d462892467 | |||
cabedb6f0b | |||
5b7f5160b2 | |||
0a8e415af8 | |||
7b3ddd58c6 | |||
ed1032415e | |||
35b43e7438 | |||
284c2e9726 | |||
fd049edb3f | |||
4884abee76 | |||
1df76b6ac3 | |||
4c42a1d511 | |||
27635a6dd3 | |||
0175425708 | |||
5e12bf60b5 | |||
2f10ca8599 | |||
62ec18309e | |||
87361bf3d7 | |||
dc45136077 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -224,7 +224,6 @@ bootstrap-custom.min.css
|
|||||||
bootstrap-custom.css
|
bootstrap-custom.css
|
||||||
**/Master/static
|
**/Master/static
|
||||||
**/Master/dev_env
|
**/Master/dev_env
|
||||||
/WebfrontCore/Views/Plugins/*
|
|
||||||
/WebfrontCore/wwwroot/**/dds
|
/WebfrontCore/wwwroot/**/dds
|
||||||
/WebfrontCore/wwwroot/images/radar/*
|
/WebfrontCore/wwwroot/images/radar/*
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IW4MAdmin.Application.Misc;
|
using IW4MAdmin.Application.Plugin;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RestEase;
|
using RestEase;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
|
55
Application/Alerts/AlertExtensions.cs
Normal file
55
Application/Alerts/AlertExtensions.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Alerts;
|
||||||
|
|
||||||
|
public static class AlertExtensions
|
||||||
|
{
|
||||||
|
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
|
||||||
|
{
|
||||||
|
return new Alert.AlertState
|
||||||
|
{
|
||||||
|
RecipientId = client.ClientId,
|
||||||
|
Category = type ?? Alert.AlertCategory.Information
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
|
||||||
|
{
|
||||||
|
state.Category = category;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
|
||||||
|
{
|
||||||
|
state.Type = type;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
|
||||||
|
{
|
||||||
|
state.Message = message;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
|
||||||
|
{
|
||||||
|
state.ExpiresAt = DateTime.Now.Add(expiration);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
|
||||||
|
{
|
||||||
|
state.Source = source;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
|
||||||
|
{
|
||||||
|
state.Source = client.Name.StripColors();
|
||||||
|
state.SourceId = client.ClientId;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
172
Application/Alerts/AlertManager.cs
Normal file
172
Application/Alerts/AlertManager.cs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Alerts;
|
||||||
|
|
||||||
|
public class AlertManager : IAlertManager
|
||||||
|
{
|
||||||
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
|
||||||
|
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
|
||||||
|
private readonly SemaphoreSlim _onModifyingAlerts = new(1, 1);
|
||||||
|
|
||||||
|
public AlertManager(ApplicationConfiguration appConfig)
|
||||||
|
{
|
||||||
|
_appConfig = appConfig;
|
||||||
|
_states.TryAdd(0, new List<Alert.AlertState>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
|
||||||
|
|
||||||
|
public async Task Initialize()
|
||||||
|
{
|
||||||
|
foreach (var source in _staticSources)
|
||||||
|
{
|
||||||
|
var alerts = await source();
|
||||||
|
foreach (var alert in alerts)
|
||||||
|
{
|
||||||
|
AddAlert(alert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Wait();
|
||||||
|
var alerts = Enumerable.Empty<Alert.AlertState>();
|
||||||
|
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
|
||||||
|
{
|
||||||
|
alerts = alerts.Concat(_states[0].Where(alert =>
|
||||||
|
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_states.ContainsKey(client.ClientId))
|
||||||
|
{
|
||||||
|
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
return alerts.OrderByDescending(alert => alert.OccuredAt).ToList();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onModifyingAlerts.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkAlertAsRead(Guid alertId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Wait();
|
||||||
|
foreach (var items in _states.Values)
|
||||||
|
{
|
||||||
|
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
|
||||||
|
|
||||||
|
if (matchingEvent is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Remove(matchingEvent);
|
||||||
|
OnAlertConsumed?.Invoke(this, matchingEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onModifyingAlerts.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkAllAlertsAsRead(int recipientId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Wait();
|
||||||
|
foreach (var items in _states.Values)
|
||||||
|
{
|
||||||
|
items.RemoveAll(item =>
|
||||||
|
{
|
||||||
|
if (item.RecipientId != null && item.RecipientId != recipientId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnAlertConsumed?.Invoke(this, item);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onModifyingAlerts.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAlert(Alert.AlertState alert)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Wait();
|
||||||
|
if (alert.RecipientId is null)
|
||||||
|
{
|
||||||
|
_states[0].Add(alert);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_states.ContainsKey(alert.RecipientId.Value))
|
||||||
|
{
|
||||||
|
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
|
||||||
|
{
|
||||||
|
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
|
||||||
|
}
|
||||||
|
|
||||||
|
_states[alert.RecipientId.Value].Add(alert);
|
||||||
|
|
||||||
|
PruneOldAlerts();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onModifyingAlerts.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onModifyingAlerts.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
|
||||||
|
{
|
||||||
|
_staticSources.Add(alertSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void PruneOldAlerts()
|
||||||
|
{
|
||||||
|
foreach (var value in _states.Values)
|
||||||
|
{
|
||||||
|
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,19 +24,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jint" Version="3.0.0-beta-2037" />
|
<PackageReference Include="Jint" Version="3.0.0-beta-2049" />
|
||||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="RestEase" Version="1.5.5" />
|
<PackageReference Include="RestEase" Version="1.5.7" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
<PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<TieredCompilation>true</TieredCompilation>
|
<TieredCompilation>true</TieredCompilation>
|
||||||
<LangVersion>Latest</LangVersion>
|
<LangVersion>Latest</LangVersion>
|
||||||
|
@ -16,6 +16,7 @@ using System.Collections;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -23,11 +24,18 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
using Data.Context;
|
using Data.Context;
|
||||||
|
using Data.Models;
|
||||||
|
using IW4MAdmin.Application.Configuration;
|
||||||
using IW4MAdmin.Application.Migration;
|
using IW4MAdmin.Application.Migration;
|
||||||
|
using IW4MAdmin.Application.Plugin.Script;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
|
using SharedLibraryCore.Events;
|
||||||
|
using SharedLibraryCore.Events.Management;
|
||||||
|
using SharedLibraryCore.Events.Server;
|
||||||
using SharedLibraryCore.Formatting;
|
using SharedLibraryCore.Formatting;
|
||||||
|
using SharedLibraryCore.Interfaces.Events;
|
||||||
using static SharedLibraryCore.GameEvent;
|
using static SharedLibraryCore.GameEvent;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
|
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
|
||||||
@ -46,8 +54,10 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
public IList<IRConParser> AdditionalRConParsers { get; }
|
public IList<IRConParser> AdditionalRConParsers { get; }
|
||||||
public IList<IEventParser> AdditionalEventParsers { get; }
|
public IList<IEventParser> AdditionalEventParsers { get; }
|
||||||
|
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
|
||||||
|
new List<Func<GameEvent, bool>>();
|
||||||
public ITokenAuthentication TokenAuthenticator { get; }
|
public ITokenAuthentication TokenAuthenticator { get; }
|
||||||
public CancellationToken CancellationToken => _tokenSource.Token;
|
public CancellationToken CancellationToken => _isRunningTokenSource.Token;
|
||||||
public string ExternalIPAddress { get; private set; }
|
public string ExternalIPAddress { get; private set; }
|
||||||
public bool IsRestartRequested { get; private set; }
|
public bool IsRestartRequested { get; private set; }
|
||||||
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
|
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
|
||||||
@ -57,53 +67,56 @@ namespace IW4MAdmin.Application
|
|||||||
private readonly List<MessageToken> MessageTokens;
|
private readonly List<MessageToken> MessageTokens;
|
||||||
private readonly ClientService ClientSvc;
|
private readonly ClientService ClientSvc;
|
||||||
readonly PenaltyService PenaltySvc;
|
readonly PenaltyService PenaltySvc;
|
||||||
|
private readonly IAlertManager _alertManager;
|
||||||
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||||
readonly IPageList PageList;
|
readonly IPageList PageList;
|
||||||
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||||
private readonly CancellationTokenSource _tokenSource;
|
private CancellationTokenSource _isRunningTokenSource;
|
||||||
|
private CancellationTokenSource _eventHandlerTokenSource;
|
||||||
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
|
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
|
||||||
private readonly ITranslationLookup _translationLookup;
|
private readonly ITranslationLookup _translationLookup;
|
||||||
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
|
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
|
||||||
private readonly IGameServerInstanceFactory _serverInstanceFactory;
|
private readonly IGameServerInstanceFactory _serverInstanceFactory;
|
||||||
private readonly IParserRegexFactory _parserRegexFactory;
|
private readonly IParserRegexFactory _parserRegexFactory;
|
||||||
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
|
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
|
||||||
private readonly IEventHandler _eventHandler;
|
private readonly ICoreEventHandler _coreEventHandler;
|
||||||
private readonly IScriptCommandFactory _scriptCommandFactory;
|
private readonly IScriptCommandFactory _scriptCommandFactory;
|
||||||
private readonly IMetaRegistration _metaRegistration;
|
private readonly IMetaRegistration _metaRegistration;
|
||||||
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
|
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ChangeHistoryService _changeHistoryService;
|
private readonly ChangeHistoryService _changeHistoryService;
|
||||||
private readonly ApplicationConfiguration _appConfig;
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
|
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new();
|
||||||
|
|
||||||
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
|
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
|
||||||
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
|
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
|
||||||
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
|
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
|
||||||
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
||||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
||||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
||||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
|
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
|
||||||
{
|
{
|
||||||
MiddlewareActionHandler = actionHandler;
|
MiddlewareActionHandler = actionHandler;
|
||||||
_servers = new ConcurrentBag<Server>();
|
_servers = new ConcurrentBag<Server>();
|
||||||
MessageTokens = new List<MessageToken>();
|
MessageTokens = new List<MessageToken>();
|
||||||
ClientSvc = clientService;
|
ClientSvc = clientService;
|
||||||
PenaltySvc = penaltyService;
|
PenaltySvc = penaltyService;
|
||||||
|
_alertManager = alertManager;
|
||||||
ConfigHandler = appConfigHandler;
|
ConfigHandler = appConfigHandler;
|
||||||
StartTime = DateTime.UtcNow;
|
StartTime = DateTime.UtcNow;
|
||||||
PageList = new PageList();
|
PageList = new PageList();
|
||||||
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
|
AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
|
||||||
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
|
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
|
||||||
TokenAuthenticator = new TokenAuthentication();
|
TokenAuthenticator = new TokenAuthentication();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_tokenSource = new CancellationTokenSource();
|
_isRunningTokenSource = new CancellationTokenSource();
|
||||||
_commands = commands.ToList();
|
_commands = commands.ToList();
|
||||||
_translationLookup = translationLookup;
|
_translationLookup = translationLookup;
|
||||||
_commandConfiguration = commandConfiguration;
|
_commandConfiguration = commandConfiguration;
|
||||||
_serverInstanceFactory = serverInstanceFactory;
|
_serverInstanceFactory = serverInstanceFactory;
|
||||||
_parserRegexFactory = parserRegexFactory;
|
_parserRegexFactory = parserRegexFactory;
|
||||||
_customParserEvents = customParserEvents;
|
_customParserEvents = customParserEvents;
|
||||||
_eventHandler = eventHandler;
|
_coreEventHandler = coreEventHandler;
|
||||||
_scriptCommandFactory = scriptCommandFactory;
|
_scriptCommandFactory = scriptCommandFactory;
|
||||||
_metaRegistration = metaRegistration;
|
_metaRegistration = metaRegistration;
|
||||||
_scriptPluginServiceResolver = scriptPluginServiceResolver;
|
_scriptPluginServiceResolver = scriptPluginServiceResolver;
|
||||||
@ -111,13 +124,17 @@ namespace IW4MAdmin.Application
|
|||||||
_changeHistoryService = changeHistoryService;
|
_changeHistoryService = changeHistoryService;
|
||||||
_appConfig = appConfig;
|
_appConfig = appConfig;
|
||||||
Plugins = plugins;
|
Plugins = plugins;
|
||||||
|
InteractionRegistration = interactionRegistration;
|
||||||
|
|
||||||
|
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IPlugin> Plugins { get; }
|
public IEnumerable<IPlugin> Plugins { get; }
|
||||||
|
public IInteractionRegistration InteractionRegistration { get; }
|
||||||
|
|
||||||
public async Task ExecuteEvent(GameEvent newEvent)
|
public async Task ExecuteEvent(GameEvent newEvent)
|
||||||
{
|
{
|
||||||
ProcessingEvents.TryAdd(newEvent.Id, newEvent);
|
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
|
||||||
|
|
||||||
// the event has failed already
|
// the event has failed already
|
||||||
if (newEvent.Failed)
|
if (newEvent.Failed)
|
||||||
@ -135,12 +152,12 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
|
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
|
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this happens if a plugin requires login
|
// this happens if a plugin requires login
|
||||||
@ -179,11 +196,11 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
skip:
|
skip:
|
||||||
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
|
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
|
||||||
{
|
{
|
||||||
var correlatedEvents =
|
var correlatedEvents =
|
||||||
ProcessingEvents.Values.Where(ev =>
|
ProcessingEvents.Values.Where(ev =>
|
||||||
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
|
ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await Task.WhenAll(correlatedEvents.Select(ev =>
|
await Task.WhenAll(correlatedEvents.Select(ev =>
|
||||||
@ -192,14 +209,16 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
foreach (var correlatedEvent in correlatedEvents)
|
foreach (var correlatedEvent in correlatedEvents)
|
||||||
{
|
{
|
||||||
ProcessingEvents.Remove(correlatedEvent.Id, out _);
|
ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't want to remove events that are correlated to command
|
// we don't want to remove events that are correlated to command
|
||||||
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
|
if (ProcessingEvents.Values.Count(gameEvent =>
|
||||||
|
newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
|
||||||
|
newEvent.CorrelationId is null)
|
||||||
{
|
{
|
||||||
ProcessingEvents.Remove(newEvent.Id, out _);
|
ProcessingEvents.Remove(newEvent.IncrementalId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell anyone waiting for the output that we're done
|
// tell anyone waiting for the output that we're done
|
||||||
@ -219,77 +238,58 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
|
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
|
||||||
|
|
||||||
public async Task UpdateServerStates()
|
private Task UpdateServerStates()
|
||||||
{
|
{
|
||||||
// store the server hash code and task for it
|
var index = 0;
|
||||||
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
|
return Task.WhenAll(_servers.Select(server =>
|
||||||
|
|
||||||
while (!_tokenSource.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
// select the server ids that have completed the update task
|
var thisIndex = index;
|
||||||
var serverTasksToRemove = runningUpdateTasks
|
Interlocked.Increment(ref index);
|
||||||
.Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
|
return ProcessUpdateHandler(server, thisIndex);
|
||||||
ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes
|
}));
|
||||||
ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5))
|
|
||||||
.Select(ut => ut.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// remove the update tasks as they have completed
|
|
||||||
foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
|
|
||||||
{
|
|
||||||
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
runningUpdateTasks[serverId].tokenSource.Cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runningUpdateTasks.Remove(serverId);
|
private async Task ProcessUpdateHandler(Server server, int index)
|
||||||
}
|
|
||||||
|
|
||||||
// select the servers where the tasks have completed
|
|
||||||
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
|
|
||||||
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
|
|
||||||
{
|
{
|
||||||
var tokenSource = new CancellationTokenSource();
|
const int delayScalar = 50; // Task.Delay is inconsistent enough there's no reason to try to prevent collisions
|
||||||
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
|
var timeout = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
while (!_isRunningTokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (runningUpdateTasks.ContainsKey(server.EndPoint))
|
var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index);
|
||||||
{
|
await Task.Delay(delayFactor, _isRunningTokenSource.Token);
|
||||||
await server.ProcessUpdatesAsync(_tokenSource.Token)
|
|
||||||
.WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception e)
|
using var timeoutTokenSource = new CancellationTokenSource();
|
||||||
{
|
timeoutTokenSource.CancelAfter(timeout);
|
||||||
using (LogContext.PushProperty("Server", server.ToString()))
|
using var linkedTokenSource =
|
||||||
{
|
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token,
|
||||||
_logger.LogError(e, "Failed to update status");
|
_isRunningTokenSource.Token);
|
||||||
}
|
await server.ProcessUpdatesAsync(linkedTokenSource.Token);
|
||||||
}
|
|
||||||
|
|
||||||
|
await Task.Delay(Math.Max(1000, _appConfig.RConPollRate - delayFactor),
|
||||||
|
_isRunningTokenSource.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("Server", server.Id))
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to update status");
|
||||||
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
server.IsInitialized = true;
|
server.IsInitialized = true;
|
||||||
}
|
}
|
||||||
}, tokenSource.Token), tokenSource, DateTime.Now));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
// run the final updates to clean up server
|
||||||
{
|
await server.ProcessUpdatesAsync(_isRunningTokenSource.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Init()
|
public async Task Init()
|
||||||
@ -300,25 +300,34 @@ namespace IW4MAdmin.Application
|
|||||||
#region DATABASE
|
#region DATABASE
|
||||||
_logger.LogInformation("Beginning database migration sync");
|
_logger.LogInformation("Beginning database migration sync");
|
||||||
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
|
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
|
||||||
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
|
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
|
||||||
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
|
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
|
||||||
_logger.LogInformation("Finished database migration sync");
|
_logger.LogInformation("Finished database migration sync");
|
||||||
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
|
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region EVENTS
|
||||||
|
IGameServerEventSubscriptions.ServerValueRequested += OnServerValueRequested;
|
||||||
|
IGameServerEventSubscriptions.ServerValueSetRequested += OnServerValueSetRequested;
|
||||||
|
IGameServerEventSubscriptions.ServerCommandExecuteRequested += OnServerCommandExecuteRequested;
|
||||||
|
await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
|
||||||
|
# endregion
|
||||||
|
|
||||||
#region PLUGINS
|
#region PLUGINS
|
||||||
foreach (var plugin in Plugins)
|
foreach (var plugin in Plugins)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (plugin is ScriptPlugin scriptPlugin)
|
if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
|
||||||
{
|
{
|
||||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
|
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
|
||||||
|
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
|
||||||
scriptPlugin.Watcher.Changed += async (sender, e) =>
|
scriptPlugin.Watcher.Changed += async (sender, e) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
|
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
|
||||||
|
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -384,13 +393,11 @@ namespace IW4MAdmin.Application
|
|||||||
if (string.IsNullOrEmpty(_appConfig.Id))
|
if (string.IsNullOrEmpty(_appConfig.Id))
|
||||||
{
|
{
|
||||||
_appConfig.Id = Guid.NewGuid().ToString();
|
_appConfig.Id = Guid.NewGuid().ToString();
|
||||||
await ConfigHandler.Save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
|
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
|
||||||
{
|
{
|
||||||
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
|
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
|
||||||
await ConfigHandler.Save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable 618
|
#pragma warning disable 618
|
||||||
@ -435,8 +442,8 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
serverConfig.ModifyParsers();
|
serverConfig.ModifyParsers();
|
||||||
}
|
}
|
||||||
await ConfigHandler.Save();
|
|
||||||
}
|
}
|
||||||
|
await ConfigHandler.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_appConfig.Servers.Length == 0)
|
if (_appConfig.Servers.Length == 0)
|
||||||
@ -461,7 +468,7 @@ namespace IW4MAdmin.Application
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region COMMANDS
|
#region COMMANDS
|
||||||
if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
|
if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
|
||||||
{
|
{
|
||||||
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
|
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
|
||||||
}
|
}
|
||||||
@ -508,6 +515,7 @@ namespace IW4MAdmin.Application
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
_metaRegistration.Register();
|
_metaRegistration.Register();
|
||||||
|
await _alertManager.Initialize();
|
||||||
|
|
||||||
#region CUSTOM_EVENTS
|
#region CUSTOM_EVENTS
|
||||||
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
||||||
@ -535,26 +543,23 @@ namespace IW4MAdmin.Application
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// todo: this might not always be an IW4MServer
|
// todo: this might not always be an IW4MServer
|
||||||
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
|
var serverInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
|
||||||
using (LogContext.PushProperty("Server", ServerInstance.ToString()))
|
using (LogContext.PushProperty("Server", serverInstance!.ToString()))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Beginning server communication initialization");
|
_logger.LogInformation("Beginning server communication initialization");
|
||||||
await ServerInstance.Initialize();
|
await serverInstance.Initialize();
|
||||||
|
|
||||||
_servers.Add(ServerInstance);
|
_servers.Add(serverInstance);
|
||||||
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
|
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(serverInstance.Hostname.StripColors()));
|
||||||
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", ServerInstance.Hostname);
|
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", serverInstance.Hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the start event for this server
|
QueueEvent(new MonitorStartEvent
|
||||||
var e = new GameEvent()
|
|
||||||
{
|
{
|
||||||
Type = EventType.Start,
|
Server = serverInstance,
|
||||||
Data = $"{ServerInstance.GameName} started",
|
Source = this
|
||||||
Owner = ServerInstance
|
});
|
||||||
};
|
|
||||||
|
|
||||||
AddEvent(e);
|
|
||||||
successServers++;
|
successServers++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,20 +581,36 @@ namespace IW4MAdmin.Application
|
|||||||
throw lastException;
|
throw lastException;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (successServers != config.Servers.Length)
|
if (successServers != config.Servers.Length && !AppContext.TryGetSwitch("NoConfirmPrompt", out _))
|
||||||
{
|
{
|
||||||
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
|
if (!Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"].PromptBool())
|
||||||
{
|
{
|
||||||
throw lastException;
|
throw lastException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Start() => await UpdateServerStates();
|
public async Task Start()
|
||||||
|
{
|
||||||
|
_eventHandlerTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var eventHandlerThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
_coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Name = nameof(CoreEventHandler)
|
||||||
|
};
|
||||||
|
|
||||||
|
eventHandlerThread.Start();
|
||||||
|
await UpdateServerStates();
|
||||||
|
_eventHandlerTokenSource.Cancel();
|
||||||
|
eventHandlerThread.Join();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Stop()
|
public async Task Stop()
|
||||||
{
|
{
|
||||||
foreach (var plugin in Plugins)
|
foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -601,15 +622,30 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_tokenSource.Cancel();
|
_isRunningTokenSource.Cancel();
|
||||||
|
|
||||||
IsRunning = false;
|
IsRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Restart()
|
public async Task Restart()
|
||||||
{
|
{
|
||||||
IsRestartRequested = true;
|
IsRestartRequested = true;
|
||||||
Stop().GetAwaiter().GetResult();
|
await Stop();
|
||||||
|
|
||||||
|
using var subscriptionTimeoutToken = new CancellationTokenSource();
|
||||||
|
subscriptionTimeoutToken.CancelAfter(Utilities.DefaultCommandTimeout);
|
||||||
|
|
||||||
|
await IManagementEventSubscriptions.InvokeUnloadAsync(this, subscriptionTimeoutToken.Token);
|
||||||
|
|
||||||
|
IGameEventSubscriptions.ClearEventInvocations();
|
||||||
|
IGameServerEventSubscriptions.ClearEventInvocations();
|
||||||
|
IManagementEventSubscriptions.ClearEventInvocations();
|
||||||
|
|
||||||
|
_isRunningTokenSource.Dispose();
|
||||||
|
_isRunningTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
_eventHandlerTokenSource.Dispose();
|
||||||
|
_eventHandlerTokenSource = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
@ -631,7 +667,7 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
|
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
|
||||||
GetActiveClients()
|
GetActiveClients()
|
||||||
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client :
|
.FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
|
||||||
client;
|
client;
|
||||||
|
|
||||||
public ClientService GetClientService()
|
public ClientService GetClientService()
|
||||||
@ -651,7 +687,12 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
public void AddEvent(GameEvent gameEvent)
|
public void AddEvent(GameEvent gameEvent)
|
||||||
{
|
{
|
||||||
_eventHandler.HandleEvent(this, gameEvent);
|
_coreEventHandler.QueueEvent(this, gameEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QueueEvent(CoreEvent coreEvent)
|
||||||
|
{
|
||||||
|
_coreEventHandler.QueueEvent(this, coreEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPageList GetPageList()
|
public IPageList GetPageList()
|
||||||
@ -688,14 +729,166 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
public void AddAdditionalCommand(IManagerCommand command)
|
public void AddAdditionalCommand(IManagerCommand command)
|
||||||
{
|
{
|
||||||
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
|
lock (_commands)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
|
if (_commands.Any(cmd => cmd.Name == command.Name || cmd.Alias == command.Alias))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Duplicate command name or alias ({command.Name}, {command.Alias})");
|
||||||
}
|
}
|
||||||
|
|
||||||
_commands.Add(command);
|
_commands.Add(command);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
|
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
|
||||||
|
public IAlertManager AlertManager => _alertManager;
|
||||||
|
|
||||||
|
private async Task OnServerValueRequested(ServerValueRequestEvent requestEvent, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (requestEvent.Server is not IW4MServer server)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dvar<string> serverValue = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (requestEvent.DelayMs.HasValue)
|
||||||
|
{
|
||||||
|
await Task.Delay(requestEvent.DelayMs.Value, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
var waitToken = token;
|
||||||
|
using var timeoutTokenSource = new CancellationTokenSource();
|
||||||
|
using var linkedTokenSource =
|
||||||
|
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
|
||||||
|
|
||||||
|
if (requestEvent.TimeoutMs is not null)
|
||||||
|
{
|
||||||
|
timeoutTokenSource.CancelAfter(requestEvent.TimeoutMs.Value);
|
||||||
|
waitToken = linkedTokenSource.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverValue =
|
||||||
|
await server.GetDvarAsync(requestEvent.ValueName, requestEvent.FallbackValue, waitToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
QueueEvent(new ServerValueReceiveEvent
|
||||||
|
{
|
||||||
|
Server = server,
|
||||||
|
Source = server,
|
||||||
|
Response = serverValue ?? new Dvar<string> { Name = requestEvent.ValueName },
|
||||||
|
Success = serverValue is not null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnServerValueSetRequested(ServerValueSetRequestEvent requestEvent, CancellationToken token)
|
||||||
|
{
|
||||||
|
return ExecuteWrapperForServerQuery(requestEvent, token, async (innerEvent) =>
|
||||||
|
{
|
||||||
|
if (innerEvent.DelayMs.HasValue)
|
||||||
|
{
|
||||||
|
await Task.Delay(innerEvent.DelayMs.Value, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (innerEvent.TimeoutMs is not null)
|
||||||
|
{
|
||||||
|
using var timeoutTokenSource = new CancellationTokenSource(innerEvent.TimeoutMs.Value);
|
||||||
|
using var linkedTokenSource =
|
||||||
|
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
|
||||||
|
token = linkedTokenSource.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
await innerEvent.Server.SetDvarAsync(innerEvent.ValueName, innerEvent.Value, token);
|
||||||
|
}, (completed, innerEvent) =>
|
||||||
|
{
|
||||||
|
QueueEvent(new ServerValueSetCompleteEvent
|
||||||
|
{
|
||||||
|
Server = innerEvent.Server,
|
||||||
|
Source = innerEvent.Server,
|
||||||
|
Success = completed,
|
||||||
|
Value = innerEvent.Value,
|
||||||
|
ValueName = innerEvent.ValueName
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnServerCommandExecuteRequested(ServerCommandRequestExecuteEvent executeEvent, CancellationToken token)
|
||||||
|
{
|
||||||
|
return ExecuteWrapperForServerQuery(executeEvent, token, async (innerEvent) =>
|
||||||
|
{
|
||||||
|
if (innerEvent.DelayMs.HasValue)
|
||||||
|
{
|
||||||
|
await Task.Delay(innerEvent.DelayMs.Value, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (innerEvent.TimeoutMs is not null)
|
||||||
|
{
|
||||||
|
using var timeoutTokenSource = new CancellationTokenSource(innerEvent.TimeoutMs.Value);
|
||||||
|
using var linkedTokenSource =
|
||||||
|
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
|
||||||
|
token = linkedTokenSource.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
await innerEvent.Server.ExecuteCommandAsync(innerEvent.Command, token);
|
||||||
|
}, (_, __) => Task.CompletedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteWrapperForServerQuery<TEventType>(TEventType serverEvent, CancellationToken token,
|
||||||
|
Func<TEventType, Task> action, Func<bool, TEventType, Task> complete) where TEventType : GameServerEvent
|
||||||
|
{
|
||||||
|
if (serverEvent.Server is not IW4MServer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var completed = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action(serverEvent);
|
||||||
|
completed = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await complete(completed, serverEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnClientPersistentIdReceived(ClientPersistentIdReceiveEvent receiveEvent, CancellationToken token)
|
||||||
|
{
|
||||||
|
var parts = receiveEvent.PersistentId.Split(",");
|
||||||
|
|
||||||
|
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
|
||||||
|
int.TryParse(parts[1], out var low))
|
||||||
|
{
|
||||||
|
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
|
||||||
|
|
||||||
|
var penalties = await PenaltySvc
|
||||||
|
.GetActivePenaltiesByIdentifier(null, guid, receiveEvent.Client.GameName);
|
||||||
|
var banPenalty =
|
||||||
|
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||||
|
|
||||||
|
if (banPenalty is not null && receiveEvent.Client.Level != Data.Models.Client.EFClient.Permission.Banned)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
||||||
|
receiveEvent.Client, guid);
|
||||||
|
receiveEvent.Client.Ban(_translationLookup["SERVER_BAN_EVADE"].FormatExt(guid),
|
||||||
|
receiveEvent.Client.CurrentServer.AsConsoleClient(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,6 @@ foreach($localization in $localizations)
|
|||||||
{
|
{
|
||||||
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
|
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
|
||||||
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
|
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
|
||||||
$response = Invoke-WebRequest $url
|
$response = Invoke-WebRequest $url -UseBasicParsing
|
||||||
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
|
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
|
||||||
}
|
}
|
52
Application/Commands/AddClientNoteCommand.cs
Normal file
52
Application/Commands/AddClientNoteCommand.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models.Client;
|
||||||
|
using IW4MAdmin.Application.Meta;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Commands;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Commands;
|
||||||
|
|
||||||
|
public class AddClientNoteCommand : Command
|
||||||
|
{
|
||||||
|
private readonly IMetaServiceV2 _metaService;
|
||||||
|
|
||||||
|
public AddClientNoteCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(config, layout)
|
||||||
|
{
|
||||||
|
Name = "addnote";
|
||||||
|
Description = _translationLookup["COMMANDS_ADD_CLIENT_NOTE_DESCRIPTION"];
|
||||||
|
Alias = "an";
|
||||||
|
Permission = EFClient.Permission.Moderator;
|
||||||
|
RequiresTarget = true;
|
||||||
|
Arguments = new[]
|
||||||
|
{
|
||||||
|
new CommandArgument
|
||||||
|
{
|
||||||
|
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
new CommandArgument
|
||||||
|
{
|
||||||
|
Name = _translationLookup["COMMANDS_ARGS_NOTE"],
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_metaService = metaService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
var note = new ClientNoteMetaResponse
|
||||||
|
{
|
||||||
|
Note = gameEvent.Data?.Trim(),
|
||||||
|
OriginEntityId = gameEvent.Origin.ClientId,
|
||||||
|
ModifiedDate = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
await _metaService.SetPersistentMetaValue("ClientNotes", note, gameEvent.Target.ClientId);
|
||||||
|
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_NOTE_SUCCESS"]);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -33,7 +34,7 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task ExecuteAsync(GameEvent gameEvent)
|
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||||
{
|
{
|
||||||
var searchTerm = gameEvent.Data.Trim();
|
var searchTerm = gameEvent.Data.Trim();
|
||||||
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
|
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
|
||||||
@ -70,24 +71,25 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
});
|
});
|
||||||
|
|
||||||
var helpResponse = new StringBuilder();
|
var helpResponse = new StringBuilder();
|
||||||
|
var messageList = new List<string>();
|
||||||
|
|
||||||
foreach (var item in commandStrings)
|
foreach (var item in commandStrings)
|
||||||
{
|
{
|
||||||
helpResponse.Append(item.response);
|
helpResponse.Append(item.response);
|
||||||
|
|
||||||
if (item.index == 0 || item.index % 4 != 0)
|
if (item.index == 0 || item.index % 4 != 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameEvent.Origin.Tell(helpResponse.ToString());
|
messageList.Add(helpResponse.ToString());
|
||||||
helpResponse = new StringBuilder();
|
helpResponse = new StringBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
gameEvent.Origin.Tell(helpResponse.ToString());
|
messageList.Add(helpResponse.ToString());
|
||||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_MOREINFO"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
await gameEvent.Origin.TellAsync(messageList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
using Data.Models.Misc;
|
using Data.Models.Misc;
|
||||||
|
using IW4MAdmin.Application.Alerts;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
@ -16,10 +19,12 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
{
|
{
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IAlertManager _alertManager;
|
||||||
private const short MaxLength = 1024;
|
private const short MaxLength = 1024;
|
||||||
|
|
||||||
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
|
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
|
||||||
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
|
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
|
||||||
|
: base(config, layout)
|
||||||
{
|
{
|
||||||
Name = "offlinemessage";
|
Name = "offlinemessage";
|
||||||
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
|
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
|
||||||
@ -29,6 +34,51 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
|
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_alertManager = alertManager;
|
||||||
|
|
||||||
|
_alertManager.RegisterStaticAlertSource(async () =>
|
||||||
|
{
|
||||||
|
var context = contextFactory.CreateContext(false);
|
||||||
|
return await context.InboxMessages.Where(message => !message.IsDelivered)
|
||||||
|
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
|
||||||
|
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
|
||||||
|
.Select(message => new Alert.AlertState
|
||||||
|
{
|
||||||
|
OccuredAt = message.CreatedDateTime,
|
||||||
|
Message = message.Message,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddDays(7),
|
||||||
|
Category = Alert.AlertCategory.Message,
|
||||||
|
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
|
||||||
|
SourceId = message.SourceClientId,
|
||||||
|
RecipientId = message.DestinationClientId,
|
||||||
|
ReferenceId = message.InboxMessageId,
|
||||||
|
Type = nameof(EFInboxMessage)
|
||||||
|
}).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
_alertManager.OnAlertConsumed += (_, state) =>
|
||||||
|
{
|
||||||
|
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = contextFactory.CreateContext(true);
|
||||||
|
foreach (var message in context.InboxMessages
|
||||||
|
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
|
||||||
|
{
|
||||||
|
message.IsDelivered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||||
@ -47,14 +97,15 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
|
|
||||||
if (gameEvent.Target.IsIngame)
|
if (gameEvent.Target.IsIngame)
|
||||||
{
|
{
|
||||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
|
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
|
||||||
|
.FormatExt(gameEvent.Target.Name));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
|
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
|
||||||
|
|
||||||
var newMessage = new EFInboxMessage()
|
var newMessage = new EFInboxMessage
|
||||||
{
|
{
|
||||||
SourceClientId = gameEvent.Origin.ClientId,
|
SourceClientId = gameEvent.Origin.ClientId,
|
||||||
DestinationClientId = gameEvent.Target.ClientId,
|
DestinationClientId = gameEvent.Target.ClientId,
|
||||||
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
Message = gameEvent.Data,
|
Message = gameEvent.Data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
|
||||||
|
.WithMessage(gameEvent.Data.Trim())
|
||||||
|
.FromClient(gameEvent.Origin)
|
||||||
|
.OfType(nameof(EFInboxMessage))
|
||||||
|
.ExpiresIn(TimeSpan.FromDays(7)));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context.Set<EFInboxMessage>().Add(newMessage);
|
context.Set<EFInboxMessage>().Add(newMessage);
|
||||||
|
@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
Name = "readmessage";
|
Name = "readmessage";
|
||||||
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
|
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
|
||||||
Alias = "rm";
|
Alias = "rm";
|
||||||
Permission = EFClient.Permission.Flagged;
|
Permission = EFClient.Permission.User;
|
||||||
|
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -49,20 +49,13 @@ namespace IW4MAdmin.Application.Commands
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = 1;
|
await gameEvent.Origin.TellAsync(inboxItems.Select((inboxItem, index) =>
|
||||||
foreach (var inboxItem in inboxItems)
|
|
||||||
{
|
{
|
||||||
await gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
|
var header = _translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
|
||||||
.FormatExt($"{index}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name))
|
.FormatExt($"{index + 1}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name);
|
||||||
.WaitAsync();
|
|
||||||
|
|
||||||
foreach (var messageFragment in inboxItem.Message.FragmentMessageForDisplay())
|
return new[] { header }.Union(inboxItem.Message.FragmentMessageForDisplay());
|
||||||
{
|
}).SelectMany(item => item));
|
||||||
await gameEvent.Origin.Tell(messageFragment).WaitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
inboxItems.ForEach(item => { item.IsDelivered = true; });
|
inboxItems.ForEach(item => { item.IsDelivered = true; });
|
||||||
|
|
||||||
|
80
Application/Commands/SetLogLevelCommand.cs
Normal file
80
Application/Commands/SetLogLevelCommand.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models.Client;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Commands;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Commands;
|
||||||
|
|
||||||
|
public class SetLogLevelCommand : Command
|
||||||
|
{
|
||||||
|
private readonly Func<string, LoggingLevelSwitch> _levelSwitchResolver;
|
||||||
|
|
||||||
|
public SetLogLevelCommand(CommandConfiguration config, ITranslationLookup layout, Func<string, LoggingLevelSwitch> levelSwitchResolver) : base(config, layout)
|
||||||
|
{
|
||||||
|
_levelSwitchResolver = levelSwitchResolver;
|
||||||
|
|
||||||
|
Name = "loglevel";
|
||||||
|
Alias = "ll";
|
||||||
|
Description = "set minimum logging level";
|
||||||
|
Permission = EFClient.Permission.Owner;
|
||||||
|
Arguments = new CommandArgument[]
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Log Level",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Override",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "IsDevelopment",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
var args = gameEvent.Data.Split(" ");
|
||||||
|
if (!Enum.TryParse<LogEventLevel>(args[0], out var minLevel))
|
||||||
|
{
|
||||||
|
await gameEvent.Origin.TellAsync(new[]
|
||||||
|
{
|
||||||
|
$"Valid log values: {string.Join(",", Enum.GetValues<LogEventLevel>())}"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = string.Empty;
|
||||||
|
|
||||||
|
if (args.Length > 1)
|
||||||
|
{
|
||||||
|
context = args[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var loggingSwitch = _levelSwitchResolver(context);
|
||||||
|
loggingSwitch.MinimumLevel = minLevel;
|
||||||
|
|
||||||
|
if (args.Length > 2 && (args[2] == "1" || args[2].ToLower() == "true"))
|
||||||
|
{
|
||||||
|
AppContext.SetSwitch("IsDevelop", true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppContext.SetSwitch("IsDevelop", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await gameEvent.Origin.TellAsync(new[]
|
||||||
|
{ $"Set minimum log level to {loggingSwitch.MinimumLevel.ToString()}" });
|
||||||
|
}
|
||||||
|
}
|
145
Application/CoreEventHandler.cs
Normal file
145
Application/CoreEventHandler.cs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Events;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore.Events.Management;
|
||||||
|
using SharedLibraryCore.Events.Server;
|
||||||
|
using SharedLibraryCore.Interfaces.Events;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application
|
||||||
|
{
|
||||||
|
public class CoreEventHandler : ICoreEventHandler
|
||||||
|
{
|
||||||
|
private const int MaxCurrentEvents = 25;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly SemaphoreSlim _onProcessingEvents = new(MaxCurrentEvents, MaxCurrentEvents);
|
||||||
|
private readonly ManualResetEventSlim _onEventReady = new(false);
|
||||||
|
private readonly ConcurrentQueue<(IManager, CoreEvent)> _runningEventTasks = new();
|
||||||
|
private CancellationToken _cancellationToken;
|
||||||
|
private int _activeTasks;
|
||||||
|
|
||||||
|
private static readonly GameEvent.EventType[] OverrideEvents =
|
||||||
|
{
|
||||||
|
GameEvent.EventType.Connect,
|
||||||
|
GameEvent.EventType.Disconnect,
|
||||||
|
GameEvent.EventType.Quit,
|
||||||
|
GameEvent.EventType.Stop
|
||||||
|
};
|
||||||
|
|
||||||
|
public CoreEventHandler(ILogger<CoreEventHandler> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QueueEvent(IManager manager, CoreEvent coreEvent)
|
||||||
|
{
|
||||||
|
_runningEventTasks.Enqueue((manager, coreEvent));
|
||||||
|
_onEventReady.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartProcessing(CancellationToken token)
|
||||||
|
{
|
||||||
|
_cancellationToken = token;
|
||||||
|
|
||||||
|
while (!_cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_onEventReady.Reset();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_onProcessingEvents.Wait(_cancellationToken);
|
||||||
|
|
||||||
|
if (!_runningEventTasks.TryDequeue(out var coreEvent))
|
||||||
|
{
|
||||||
|
if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
|
||||||
|
{
|
||||||
|
_onProcessingEvents.Release(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onEventReady.Wait(_cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Start processing event {Name} {SemaphoreCount} - {QueuedTasks}",
|
||||||
|
coreEvent.Item2.GetType().Name, _onProcessingEvents.CurrentCount, _runningEventTasks.Count);
|
||||||
|
|
||||||
|
_ = Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _activeTasks);
|
||||||
|
_logger.LogDebug("[Start] Active Tasks = {TaskCount}", _activeTasks);
|
||||||
|
return HandleEventTaskExecute(coreEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not enqueue event for processing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleEventTaskExecute((IManager, CoreEvent) coreEvent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await GetEventTask(coreEvent.Item1, coreEvent.Item2);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Event timed out {Type}", coreEvent.Item2.GetType().Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not complete invoke for {EventType}",
|
||||||
|
coreEvent.Item2.GetType().Name);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Freeing up event semaphore for next event {SemaphoreCount}",
|
||||||
|
_onProcessingEvents.CurrentCount);
|
||||||
|
_onProcessingEvents.Release(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Decrement(ref _activeTasks);
|
||||||
|
_logger.LogDebug("[Complete] {Type}, Active Tasks = {TaskCount} - {Queue}", coreEvent.Item2.GetType(),
|
||||||
|
_activeTasks, _runningEventTasks.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task GetEventTask(IManager manager, CoreEvent coreEvent)
|
||||||
|
{
|
||||||
|
return coreEvent switch
|
||||||
|
{
|
||||||
|
GameEvent gameEvent => BuildLegacyEventTask(manager, coreEvent, gameEvent),
|
||||||
|
GameServerEvent gameServerEvent => IGameServerEventSubscriptions.InvokeEventAsync(gameServerEvent,
|
||||||
|
manager.CancellationToken),
|
||||||
|
ManagementEvent managementEvent => IManagementEventSubscriptions.InvokeEventAsync(managementEvent,
|
||||||
|
manager.CancellationToken),
|
||||||
|
_ => Task.CompletedTask
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BuildLegacyEventTask(IManager manager, CoreEvent coreEvent, GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
if (manager.IsRunning || OverrideEvents.Contains(gameEvent.Type))
|
||||||
|
{
|
||||||
|
await manager.ExecuteEvent(gameEvent);
|
||||||
|
await IGameEventSubscriptions.InvokeEventAsync(coreEvent, manager.CancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Skipping event as we're shutting down {EventId}", gameEvent.IncrementalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -69,6 +69,39 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Gametypes": [
|
"Gametypes": [
|
||||||
|
{
|
||||||
|
"Game": "IW3",
|
||||||
|
"Gametypes": [
|
||||||
|
{
|
||||||
|
"Name": "ctf",
|
||||||
|
"Alias": "Capture The Flag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dm",
|
||||||
|
"Alias": "Free For All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dom",
|
||||||
|
"Alias": "Domination"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "koth",
|
||||||
|
"Alias": "Headquarters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "sab",
|
||||||
|
"Alias": "Sabotage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "sd",
|
||||||
|
"Alias": "Search & Destroy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "war",
|
||||||
|
"Alias": "Team Deathmatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Game": "IW4",
|
"Game": "IW4",
|
||||||
"Gametypes": [
|
"Gametypes": [
|
||||||
@ -152,20 +185,24 @@
|
|||||||
{
|
{
|
||||||
"Name": "twar",
|
"Name": "twar",
|
||||||
"Alias": "War"
|
"Alias": "War"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "cmp",
|
||||||
|
"Alias": "Zombies"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Game": "IW5",
|
"Game": "IW5",
|
||||||
"Gametypes": [
|
"Gametypes": [
|
||||||
{
|
|
||||||
"Name": "tdm",
|
|
||||||
"Alias": "Team Deathmatch"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Name": "dom",
|
"Name": "dom",
|
||||||
"Alias": "Domination"
|
"Alias": "Domination"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "conf",
|
||||||
|
"Alias": "Kill Confirmed"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "ctf",
|
"Name": "ctf",
|
||||||
"Alias": "Capture The Flag"
|
"Alias": "Capture The Flag"
|
||||||
@ -175,37 +212,29 @@
|
|||||||
"Alias": "Demolition"
|
"Alias": "Demolition"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "dz",
|
"Name": "dm",
|
||||||
"Alias": "Drop Zone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "ffa",
|
|
||||||
"Alias": "Free For All"
|
"Alias": "Free For All"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "gg",
|
"Name": "grnd",
|
||||||
"Alias": "Gun Game"
|
"Alias": "Drop Zone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "hq",
|
"Name": "gun",
|
||||||
"Alias": "Headquarters"
|
"Alias": "Gun Game"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "koth",
|
"Name": "koth",
|
||||||
"Alias": "Headquarters"
|
"Alias": "Headquarters"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "inf",
|
"Name": "infect",
|
||||||
"Alias": "Infected"
|
"Alias": "Infected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "jug",
|
"Name": "jugg",
|
||||||
"Alias": "Juggernaut"
|
"Alias": "Juggernaut"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"Name": "kc",
|
|
||||||
"Alias": "Kill Confirmed"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Name": "oic",
|
"Name": "oic",
|
||||||
"Alias": "One In The Chamber"
|
"Alias": "One In The Chamber"
|
||||||
@ -223,8 +252,12 @@
|
|||||||
"Alias": "Team Defender"
|
"Alias": "Team Defender"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "tj",
|
"Name": "tjugg",
|
||||||
"Alias": "Team Juggernaut"
|
"Alias": "Team Juggernaut"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "war",
|
||||||
|
"Alias": "Team Deathmatch"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -278,6 +311,10 @@
|
|||||||
{
|
{
|
||||||
"Name": "tdm",
|
"Name": "tdm",
|
||||||
"Alias": "Team Deathmatch"
|
"Alias": "Team Deathmatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "zom",
|
||||||
|
"Alias": "Zombies"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -408,6 +445,14 @@
|
|||||||
{
|
{
|
||||||
"Name": "tdm",
|
"Name": "tdm",
|
||||||
"Alias": "Team Deathmatch"
|
"Alias": "Team Deathmatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "zclassic",
|
||||||
|
"Alias": "Zombies Classic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "zstandard",
|
||||||
|
"Alias": "Zombies"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -509,6 +554,10 @@
|
|||||||
{
|
{
|
||||||
"Name": "hc_tdm",
|
"Name": "hc_tdm",
|
||||||
"Alias": "Hardcore Team Deathmatch"
|
"Alias": "Hardcore Team Deathmatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "zclassic",
|
||||||
|
"Alias": "Zombies Classic"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -564,6 +613,55 @@
|
|||||||
"Alias": "Momentum"
|
"Alias": "Momentum"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Game": "H1",
|
||||||
|
"Gametypes": [
|
||||||
|
{
|
||||||
|
"Name": "conf",
|
||||||
|
"Alias": "Kill Confirmed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ctf",
|
||||||
|
"Alias": "Capture The Flag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dd",
|
||||||
|
"Alias": "Demolition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dm",
|
||||||
|
"Alias": "Free For All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dom",
|
||||||
|
"Alias": "Domination"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "gun",
|
||||||
|
"Alias": "Gun Game"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "hp",
|
||||||
|
"Alias": "Hardpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "koth",
|
||||||
|
"Alias": "Headquarters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "sab",
|
||||||
|
"Alias": "Sabotage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "sd",
|
||||||
|
"Alias": "Search & Destroy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "war",
|
||||||
|
"Alias": "Team Deathmatch"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Maps": [
|
"Maps": [
|
||||||
@ -750,6 +848,22 @@
|
|||||||
{
|
{
|
||||||
"Alias": "Upheaval",
|
"Alias": "Upheaval",
|
||||||
"Name": "mp_suburban"
|
"Name": "mp_suburban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Nacht Der Untoten",
|
||||||
|
"Name": "nazi_zombie_prototype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Verrückt",
|
||||||
|
"Name": "nazi_zombie_asylum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shi No Numa",
|
||||||
|
"Name": "nazi_zombie_sumpf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Der Riese",
|
||||||
|
"Name": "nazi_zombie_factory"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -935,6 +1049,82 @@
|
|||||||
{
|
{
|
||||||
"Alias": "Village",
|
"Alias": "Village",
|
||||||
"Name": "co_hunted"
|
"Name": "co_hunted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Broadcast",
|
||||||
|
"Name": "mp_broadcast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Showdown",
|
||||||
|
"Name": "mp_showdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Ambush",
|
||||||
|
"Name": "mp_convoy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "District",
|
||||||
|
"Name": "mp_citystreets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Backlot",
|
||||||
|
"Name": "mp_backlot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Pipeline",
|
||||||
|
"Name": "mp_pipeline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Chinatown",
|
||||||
|
"Name": "mp_carentan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Winter Crash",
|
||||||
|
"Name": "mp_crash_snow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Countdown",
|
||||||
|
"Name": "mp_countdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Downpour",
|
||||||
|
"Name": "mp_farm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Dome",
|
||||||
|
"Name": "mp_dome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hardhat",
|
||||||
|
"Name": "mp_hardhat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Resistance",
|
||||||
|
"Name": "mp_paris"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Seatown",
|
||||||
|
"Name": "mp_seatown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Mission",
|
||||||
|
"Name": "mp_bravo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Underground",
|
||||||
|
"Name": "mp_underground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Arkaden",
|
||||||
|
"Name": "mp_plaza2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Village",
|
||||||
|
"Name": "mp_village"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Lockdown",
|
||||||
|
"Name": "mp_alpha"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1044,6 +1234,46 @@
|
|||||||
{
|
{
|
||||||
"Alias": "Zoo",
|
"Alias": "Zoo",
|
||||||
"Name": "mp_zoo"
|
"Name": "mp_zoo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Kino der Toten",
|
||||||
|
"Name": "zombie_theater"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Five",
|
||||||
|
"Name": "zombie_pentagon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Ascension",
|
||||||
|
"Name": "zombie_cosmodrome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Call of the Dead",
|
||||||
|
"Name": "zombie_coast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shangri-La",
|
||||||
|
"Name": "zombie_temple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Moon",
|
||||||
|
"Name": "zombie_moon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Nacht Der Untoten",
|
||||||
|
"Name": "zombie_cod5_prototype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Verrückt",
|
||||||
|
"Name": "zombie_cod5_asylum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shi No Numa",
|
||||||
|
"Name": "zombie_cod5_sumpf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Der Riese",
|
||||||
|
"Name": "zombie_cod5_factory"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1491,6 +1721,70 @@
|
|||||||
{
|
{
|
||||||
"Alias": "Outlaw",
|
"Alias": "Outlaw",
|
||||||
"Name": "mp_western"
|
"Name": "mp_western"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Fringe Night",
|
||||||
|
"Name": "mp_veiled_heyday"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Redwood Snow",
|
||||||
|
"Name": "mp_redwood_ice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shadows of Evil",
|
||||||
|
"Name": "zm_zod"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Der Eisendrache",
|
||||||
|
"Name": "zm_castle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Zetsubou No Shima",
|
||||||
|
"Name": "zm_island"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Gorod Krovi",
|
||||||
|
"Name": "zm_stalingrad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Revelations",
|
||||||
|
"Name": "zm_genesis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Ascension",
|
||||||
|
"Name": "zm_cosmodrome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Kino der Toten",
|
||||||
|
"Name": "zm_theater"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Moon",
|
||||||
|
"Name": "zm_moon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Nacht der Untoten",
|
||||||
|
"Name": "zm_prototype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Origins",
|
||||||
|
"Name": "zm_tomb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shangri-La",
|
||||||
|
"Name": "zm_temple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shi No Numa",
|
||||||
|
"Name": "zm_sumpf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "The Giant",
|
||||||
|
"Name": "zm_factory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Verrückt",
|
||||||
|
"Name": "zm_asylum"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1768,6 +2062,103 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Game": "H1",
|
||||||
|
"Maps": [
|
||||||
|
{
|
||||||
|
"Alias": "Ambush",
|
||||||
|
"Name": "mp_convoy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Backlot",
|
||||||
|
"Name": "mp_backlot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Bloc",
|
||||||
|
"Name": "mp_bloc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Bog",
|
||||||
|
"Name": "mp_bog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Countdown",
|
||||||
|
"Name": "mp_countdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Crash",
|
||||||
|
"Name": "mp_crash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Crossfire",
|
||||||
|
"Name": "mp_crossfire"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "District",
|
||||||
|
"Name": "mp_citystreets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Downpour",
|
||||||
|
"Name": "mp_farm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Overgrown",
|
||||||
|
"Name": "mp_overgrown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Pipeline",
|
||||||
|
"Name": "mp_pipeline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shipment",
|
||||||
|
"Name": "mp_shipment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Showdown",
|
||||||
|
"Name": "mp_showdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Strike",
|
||||||
|
"Name": "mp_strike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Vacant",
|
||||||
|
"Name": "mp_vacant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Wet Work",
|
||||||
|
"Name": "mp_cargoship"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Winter Crash",
|
||||||
|
"Name": "mp_crash_snow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Broadcast",
|
||||||
|
"Name": "mp_broadcast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Creek",
|
||||||
|
"Name": "mp_creek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Chinatown",
|
||||||
|
"Name": "mp_carentan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Killhouse",
|
||||||
|
"Name": "mp_killhouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Day Break",
|
||||||
|
"Name": "mp_farm_spring"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Beach Bog",
|
||||||
|
"Name": "mp_bog_summer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Game": "CSGO",
|
"Game": "CSGO",
|
||||||
"Maps": [
|
"Maps": [
|
||||||
|
@ -7,6 +7,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Data.Models;
|
using Data.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore.Events.Game;
|
||||||
|
using static System.Int32;
|
||||||
using static SharedLibraryCore.Server;
|
using static SharedLibraryCore.Server;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
@ -14,21 +16,26 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
public class BaseEventParser : IEventParser
|
public class BaseEventParser : IEventParser
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
|
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>
|
||||||
|
_customEventRegistrations;
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ApplicationConfiguration _appConfig;
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
|
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
|
||||||
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
|
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
|
||||||
|
|
||||||
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
|
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger,
|
||||||
|
ApplicationConfiguration appConfig)
|
||||||
{
|
{
|
||||||
_customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
|
_customEventRegistrations =
|
||||||
|
new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appConfig = appConfig;
|
_appConfig = appConfig;
|
||||||
|
|
||||||
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
|
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
|
||||||
{
|
{
|
||||||
GameDirectory = "main",
|
GameDirectory = "main",
|
||||||
|
LocalizeText = "\x15",
|
||||||
};
|
};
|
||||||
|
|
||||||
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
|
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
|
||||||
@ -57,7 +64,8 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginTeam, 4);
|
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginTeam, 4);
|
||||||
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginName, 5);
|
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginName, 5);
|
||||||
|
|
||||||
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
Configuration.Damage.Pattern =
|
||||||
|
@"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[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);
|
||||||
@ -72,7 +80,8 @@ 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);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
Configuration.Kill.Pattern =
|
||||||
|
@"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[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);
|
||||||
@ -104,7 +113,9 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
|
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
|
||||||
{
|
{
|
||||||
{ "say", GameEvent.EventType.Say },
|
{ "say", GameEvent.EventType.Say },
|
||||||
{"sayteam", GameEvent.EventType.Say},
|
{ "sayteam", GameEvent.EventType.SayTeam },
|
||||||
|
{ "chat", GameEvent.EventType.Say },
|
||||||
|
{ "chatteam", GameEvent.EventType.SayTeam },
|
||||||
{ "K", GameEvent.EventType.Kill },
|
{ "K", GameEvent.EventType.Kill },
|
||||||
{ "D", GameEvent.EventType.Damage },
|
{ "D", GameEvent.EventType.Damage },
|
||||||
{ "J", GameEvent.EventType.PreConnect },
|
{ "J", GameEvent.EventType.PreConnect },
|
||||||
@ -123,13 +134,536 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
|
|
||||||
public string Name { get; set; } = "Call of Duty";
|
public string Name { get; set; } = "Call of Duty";
|
||||||
|
|
||||||
|
public virtual GameEvent GenerateGameEvent(string logLine)
|
||||||
|
{
|
||||||
|
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
|
||||||
|
var gameTime = 0L;
|
||||||
|
|
||||||
|
if (timeMatch.Success)
|
||||||
|
{
|
||||||
|
if (timeMatch.Values[0].Contains(':'))
|
||||||
|
{
|
||||||
|
gameTime = timeMatch
|
||||||
|
.Values
|
||||||
|
.Skip(2)
|
||||||
|
// this converts the timestamp into seconds passed
|
||||||
|
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
|
||||||
|
.Sum();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gameTime = long.Parse(timeMatch.Values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want to strip the time from the log line
|
||||||
|
logLine = logLine[timeMatch.Values.First().Length..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (eventType, eventKey) = GetEventTypeFromLine(logLine);
|
||||||
|
|
||||||
|
switch (eventType)
|
||||||
|
{
|
||||||
|
case GameEvent.EventType.Say or GameEvent.EventType.SayTeam:
|
||||||
|
return ParseMessageEvent(logLine, gameTime, eventType) ?? GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.Kill:
|
||||||
|
return ParseKillEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.Damage:
|
||||||
|
return ParseDamageEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.PreConnect:
|
||||||
|
return ParseClientEnterMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.JoinTeam:
|
||||||
|
return ParseJoinTeamEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.PreDisconnect:
|
||||||
|
return ParseClientExitMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.MapEnd:
|
||||||
|
return ParseMatchEndEvent(logLine, gameTime);
|
||||||
|
case GameEvent.EventType.MapChange:
|
||||||
|
return ParseMatchStartEvent(logLine, gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logLine.StartsWith("GSE;"))
|
||||||
|
{
|
||||||
|
return new GameScriptEvent
|
||||||
|
{
|
||||||
|
ScriptData = logLine,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey))
|
||||||
|
{
|
||||||
|
return GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventModifier = _customEventRegistrations[eventKey];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return eventModifier.Item2(logLine, Configuration, new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Other,
|
||||||
|
Data = logLine,
|
||||||
|
Subtype = eventModifier.Item1,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not handle custom log event generation");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GenerateDefaultEvent(logLine, gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameEvent GenerateDefaultEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
return new GameEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Unknown,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
|
Target = Utilities.IW4MAdminClient(),
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameEvent ParseMatchStartEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
var dump = logLine.Replace("InitGame: ", "").DictionaryFromKeyValue();
|
||||||
|
|
||||||
|
return new MatchStartEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.MapChange,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
|
Target = Utilities.IW4MAdminClient(),
|
||||||
|
Extra = dump,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
SessionData = dump
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameEvent ParseMatchEndEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
return new MatchEndEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.MapEnd,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
|
Target = Utilities.IW4MAdminClient(),
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
SessionData = logLine
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameEvent ParseClientExitMatchEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
var match = Configuration.Quit.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originIdString =
|
||||||
|
match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
||||||
|
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
var originClientNumber =
|
||||||
|
Convert.ToInt32(
|
||||||
|
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
|
||||||
|
var networkId = originIdString.IsBotGuid()
|
||||||
|
? originName.GenerateGuidFromString()
|
||||||
|
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
|
|
||||||
|
return new ClientExitMatchEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.PreDisconnect,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = new EFClient
|
||||||
|
{
|
||||||
|
CurrentAlias = new EFAlias
|
||||||
|
{
|
||||||
|
Name = originName
|
||||||
|
},
|
||||||
|
NetworkId = networkId,
|
||||||
|
ClientNumber = originClientNumber,
|
||||||
|
State = EFClient.ClientState.Disconnecting
|
||||||
|
},
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
IsBlocking = true,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = originClientNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameEvent ParseJoinTeamEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originIdString =
|
||||||
|
match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
||||||
|
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
|
||||||
|
var clientSlotNumber =
|
||||||
|
Parse(match.Values[
|
||||||
|
Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
|
||||||
|
if (Configuration.TeamMapping.ContainsKey(team))
|
||||||
|
{
|
||||||
|
team = Configuration.TeamMapping[team].ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkId = originIdString.IsBotGuid()
|
||||||
|
? originName.GenerateGuidFromString()
|
||||||
|
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
|
|
||||||
|
return new ClientJoinTeamEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.JoinTeam,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = new EFClient
|
||||||
|
{
|
||||||
|
CurrentAlias = new EFAlias
|
||||||
|
{
|
||||||
|
Name = originName
|
||||||
|
},
|
||||||
|
NetworkId = networkId,
|
||||||
|
ClientNumber = clientSlotNumber,
|
||||||
|
State = EFClient.ClientState.Connected,
|
||||||
|
},
|
||||||
|
Extra = team,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
TeamName = team,
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = clientSlotNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameEvent ParseClientEnterMatchEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
var match = Configuration.Join.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
||||||
|
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]]
|
||||||
|
.TrimNewLine();
|
||||||
|
var originClientNumber =
|
||||||
|
Convert.ToInt32(
|
||||||
|
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
|
||||||
|
var networkId = originIdString.IsBotGuid()
|
||||||
|
? originName.GenerateGuidFromString()
|
||||||
|
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
|
|
||||||
|
return new ClientEnterMatchEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.PreConnect,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = new EFClient
|
||||||
|
{
|
||||||
|
CurrentAlias = new EFAlias
|
||||||
|
{
|
||||||
|
Name = originName
|
||||||
|
},
|
||||||
|
NetworkId = networkId,
|
||||||
|
ClientNumber = originClientNumber,
|
||||||
|
State = EFClient.ClientState.Connecting,
|
||||||
|
},
|
||||||
|
Extra = originIdString,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||||
|
IsBlocking = true,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = originClientNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region DAMAGE
|
||||||
|
|
||||||
|
private GameEvent ParseDamageEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
var match = Configuration.Damage.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
||||||
|
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
|
||||||
|
|
||||||
|
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
|
||||||
|
var originId = originIdString.IsBotGuid()
|
||||||
|
? originName.GenerateGuidFromString()
|
||||||
|
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
||||||
|
var targetId = targetIdString.IsBotGuid()
|
||||||
|
? targetName.GenerateGuidFromString()
|
||||||
|
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
||||||
|
|
||||||
|
var originClientNumber =
|
||||||
|
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
var targetClientNumber =
|
||||||
|
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
|
||||||
|
|
||||||
|
var originTeamName =
|
||||||
|
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginTeam]];
|
||||||
|
var targetTeamName =
|
||||||
|
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetTeam]];
|
||||||
|
|
||||||
|
if (Configuration.TeamMapping.ContainsKey(originTeamName))
|
||||||
|
{
|
||||||
|
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
|
||||||
|
{
|
||||||
|
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var weaponName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Weapon]];
|
||||||
|
TryParse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Damage]],
|
||||||
|
out var damage);
|
||||||
|
var meansOfDeath =
|
||||||
|
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
|
||||||
|
var hitLocation =
|
||||||
|
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.HitLocation]];
|
||||||
|
|
||||||
|
return new ClientDamageEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Damage,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
|
||||||
|
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = originClientNumber,
|
||||||
|
AttackerTeamName = originTeamName,
|
||||||
|
VictimClientName = targetName,
|
||||||
|
VictimNetworkId = targetIdString,
|
||||||
|
VictimClientSlotNumber = targetClientNumber,
|
||||||
|
VictimTeamName = targetTeamName,
|
||||||
|
WeaponName = weaponName,
|
||||||
|
Damage = damage,
|
||||||
|
MeansOfDeath = meansOfDeath,
|
||||||
|
HitLocation = hitLocation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameEvent ParseKillEvent(string logLine, long gameTime)
|
||||||
|
{
|
||||||
|
var match = Configuration.Kill.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
||||||
|
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
|
||||||
|
|
||||||
|
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
|
||||||
|
var originId = originIdString.IsBotGuid()
|
||||||
|
? originName.GenerateGuidFromString()
|
||||||
|
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
||||||
|
var targetId = targetIdString.IsBotGuid()
|
||||||
|
? targetName.GenerateGuidFromString()
|
||||||
|
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
||||||
|
|
||||||
|
var originClientNumber =
|
||||||
|
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
var targetClientNumber =
|
||||||
|
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
|
||||||
|
|
||||||
|
var originTeamName =
|
||||||
|
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginTeam]];
|
||||||
|
var targetTeamName =
|
||||||
|
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetTeam]];
|
||||||
|
|
||||||
|
if (Configuration.TeamMapping.ContainsKey(originTeamName))
|
||||||
|
{
|
||||||
|
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
|
||||||
|
{
|
||||||
|
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var weaponName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Weapon]];
|
||||||
|
TryParse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Damage]],
|
||||||
|
out var damage);
|
||||||
|
var meansOfDeath =
|
||||||
|
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
|
||||||
|
var hitLocation =
|
||||||
|
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.HitLocation]];
|
||||||
|
|
||||||
|
return new ClientKillEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Kill,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
|
||||||
|
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
// V2
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = originClientNumber,
|
||||||
|
AttackerTeamName = originTeamName,
|
||||||
|
VictimClientName = targetName,
|
||||||
|
VictimNetworkId = targetIdString,
|
||||||
|
VictimClientSlotNumber = targetClientNumber,
|
||||||
|
VictimTeamName = targetTeamName,
|
||||||
|
WeaponName = weaponName,
|
||||||
|
Damage = damage,
|
||||||
|
MeansOfDeath = meansOfDeath,
|
||||||
|
HitLocation = hitLocation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MESSAGE
|
||||||
|
|
||||||
|
private GameEvent ParseMessageEvent(string logLine, long gameTime, GameEvent.EventType eventType)
|
||||||
|
{
|
||||||
|
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
|
||||||
|
|
||||||
|
if (!matchResult.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new string(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
||||||
|
.Where(c => !char.IsControl(c)).ToArray());
|
||||||
|
|
||||||
|
if (message.StartsWith("/"))
|
||||||
|
{
|
||||||
|
message = message[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(message))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originIdString =
|
||||||
|
matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
||||||
|
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]]
|
||||||
|
?.TrimNewLine();
|
||||||
|
var clientNumber =
|
||||||
|
Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||||
|
|
||||||
|
var originId = originIdString.IsBotGuid()
|
||||||
|
? originName.GenerateGuidFromString()
|
||||||
|
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
|
|
||||||
|
|
||||||
|
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
|
||||||
|
{
|
||||||
|
return new ClientCommandEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Command,
|
||||||
|
Data = message,
|
||||||
|
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
|
||||||
|
Message = message,
|
||||||
|
Extra = logLine,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
//V2
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = clientNumber,
|
||||||
|
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClientMessageEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Say,
|
||||||
|
Data = message,
|
||||||
|
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
|
||||||
|
Message = message,
|
||||||
|
Extra = logLine,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
||||||
|
GameTime = gameTime,
|
||||||
|
Source = GameEvent.EventSource.Log,
|
||||||
|
|
||||||
|
//V2
|
||||||
|
ClientName = originName,
|
||||||
|
ClientNetworkId = originIdString,
|
||||||
|
ClientSlotNumber = clientNumber,
|
||||||
|
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
|
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
|
||||||
{
|
{
|
||||||
var lineSplit = logLine.Split(';');
|
var lineSplit = logLine.Split(';');
|
||||||
if (lineSplit.Length > 1)
|
if (lineSplit.Length > 1)
|
||||||
{
|
{
|
||||||
var type = lineSplit[0];
|
var type = lineSplit[0];
|
||||||
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
|
return _eventTypeMap.ContainsKey(type)
|
||||||
|
? (_eventTypeMap[type], type)
|
||||||
|
: (GameEvent.EventType.Unknown, lineSplit[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (key, value) in _regexMap)
|
foreach (var (key, value) in _regexMap)
|
||||||
@ -144,347 +678,9 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
return (GameEvent.EventType.Unknown, null);
|
return (GameEvent.EventType.Unknown, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public virtual GameEvent GenerateGameEvent(string logLine)
|
|
||||||
{
|
|
||||||
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
|
|
||||||
var gameTime = 0L;
|
|
||||||
|
|
||||||
if (timeMatch.Success)
|
|
||||||
{
|
|
||||||
if (timeMatch.Values[0].Contains(":"))
|
|
||||||
{
|
|
||||||
gameTime = timeMatch
|
|
||||||
.Values
|
|
||||||
.Skip(2)
|
|
||||||
// this converts the timestamp into seconds passed
|
|
||||||
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
|
|
||||||
.Sum();
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
gameTime = long.Parse(timeMatch.Values[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we want to strip the time from the log line
|
|
||||||
logLine = logLine.Substring(timeMatch.Values.First().Length).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventParseResult = GetEventTypeFromLine(logLine);
|
|
||||||
var eventType = eventParseResult.type;
|
|
||||||
|
|
||||||
_logger.LogDebug(logLine);
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.Say)
|
|
||||||
{
|
|
||||||
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
|
|
||||||
|
|
||||||
if (matchResult.Success)
|
|
||||||
{
|
|
||||||
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
|
||||||
.Replace("\x15", "")
|
|
||||||
.Trim();
|
|
||||||
|
|
||||||
if (message.Length > 0)
|
|
||||||
{
|
|
||||||
var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
|
||||||
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]];
|
|
||||||
|
|
||||||
var originId = originIdString.IsBotGuid() ?
|
|
||||||
originName.GenerateGuidFromString() :
|
|
||||||
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
|
||||||
|
|
||||||
var clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
|
||||||
|
|
||||||
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Command,
|
|
||||||
Data = message,
|
|
||||||
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
|
|
||||||
Message = message,
|
|
||||||
Extra = logLine,
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Say,
|
|
||||||
Data = message,
|
|
||||||
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
|
|
||||||
Message = message,
|
|
||||||
Extra = logLine,
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.Kill)
|
|
||||||
{
|
|
||||||
var match = Configuration.Kill.PatternMatcher.Match(logLine);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
|
||||||
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
|
|
||||||
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]];
|
|
||||||
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]];
|
|
||||||
|
|
||||||
var originId = originIdString.IsBotGuid() ?
|
|
||||||
originName.GenerateGuidFromString() :
|
|
||||||
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
|
||||||
var targetId = targetIdString.IsBotGuid() ?
|
|
||||||
targetName.GenerateGuidFromString() :
|
|
||||||
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
|
||||||
|
|
||||||
var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
|
||||||
var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Kill,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
|
|
||||||
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.Damage)
|
|
||||||
{
|
|
||||||
var match = Configuration.Damage.PatternMatcher.Match(logLine);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
|
||||||
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
|
|
||||||
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]];
|
|
||||||
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]];
|
|
||||||
|
|
||||||
var originId = originIdString.IsBotGuid() ?
|
|
||||||
originName.GenerateGuidFromString() :
|
|
||||||
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
|
||||||
var targetId = targetIdString.IsBotGuid() ?
|
|
||||||
targetName.GenerateGuidFromString() :
|
|
||||||
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
|
|
||||||
|
|
||||||
var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
|
||||||
var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Damage,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
|
|
||||||
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.PreConnect)
|
|
||||||
{
|
|
||||||
var match = Configuration.Join.PatternMatcher.Match(logLine);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
|
||||||
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]];
|
|
||||||
|
|
||||||
var networkId = originIdString.IsBotGuid() ?
|
|
||||||
originName.GenerateGuidFromString() :
|
|
||||||
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.PreConnect,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = new EFClient()
|
|
||||||
{
|
|
||||||
CurrentAlias = new EFAlias()
|
|
||||||
{
|
|
||||||
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
|
|
||||||
},
|
|
||||||
NetworkId = networkId,
|
|
||||||
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
|
|
||||||
State = EFClient.ClientState.Connecting,
|
|
||||||
},
|
|
||||||
Extra = originIdString,
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
|
||||||
IsBlocking = true,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.JoinTeam)
|
|
||||||
{
|
|
||||||
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var originIdString = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
|
||||||
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]];
|
|
||||||
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
|
|
||||||
|
|
||||||
if (Configuration.TeamMapping.ContainsKey(team))
|
|
||||||
{
|
|
||||||
team = Configuration.TeamMapping[team].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
var networkId = originIdString.IsBotGuid() ?
|
|
||||||
originName.GenerateGuidFromString() :
|
|
||||||
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
|
||||||
|
|
||||||
return new GameEvent
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.JoinTeam,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = new EFClient
|
|
||||||
{
|
|
||||||
CurrentAlias = new EFAlias
|
|
||||||
{
|
|
||||||
Name = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
|
|
||||||
},
|
|
||||||
NetworkId = networkId,
|
|
||||||
ClientNumber = Convert.ToInt32(match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
|
|
||||||
State = EFClient.ClientState.Connected,
|
|
||||||
},
|
|
||||||
Extra = team,
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.PreDisconnect)
|
|
||||||
{
|
|
||||||
var match = Configuration.Quit.PatternMatcher.Match(logLine);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
|
|
||||||
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]];
|
|
||||||
|
|
||||||
var networkId = originIdString.IsBotGuid() ?
|
|
||||||
originName.GenerateGuidFromString() :
|
|
||||||
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.PreDisconnect,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = new EFClient()
|
|
||||||
{
|
|
||||||
CurrentAlias = new EFAlias()
|
|
||||||
{
|
|
||||||
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine()
|
|
||||||
},
|
|
||||||
NetworkId = networkId,
|
|
||||||
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
|
|
||||||
State = EFClient.ClientState.Disconnecting
|
|
||||||
},
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
|
||||||
IsBlocking = true,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.MapEnd)
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.MapEnd,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = Utilities.IW4MAdminClient(),
|
|
||||||
Target = Utilities.IW4MAdminClient(),
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == GameEvent.EventType.MapChange)
|
|
||||||
{
|
|
||||||
var dump = logLine.Replace("InitGame: ", "");
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.MapChange,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = Utilities.IW4MAdminClient(),
|
|
||||||
Target = Utilities.IW4MAdminClient(),
|
|
||||||
Extra = dump.DictionaryFromKeyValue(),
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey))
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Unknown,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = Utilities.IW4MAdminClient(),
|
|
||||||
Target = Utilities.IW4MAdminClient(),
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventModifier = _customEventRegistrations[eventParseResult.eventKey];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return eventModifier.Item2(logLine, Configuration, new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Other,
|
|
||||||
Data = logLine,
|
|
||||||
Subtype = eventModifier.Item1,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Could not handle custom event generation");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Unknown,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = Utilities.IW4MAdminClient(),
|
|
||||||
Target = Utilities.IW4MAdminClient(),
|
|
||||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
|
||||||
GameTime = gameTime,
|
|
||||||
Source = GameEvent.EventSource.Log
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
|
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue,
|
||||||
|
Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(eventSubtype))
|
if (string.IsNullOrWhiteSpace(eventSubtype))
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,7 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
public string GameDirectory { get; set; }
|
public string GameDirectory { get; set; }
|
||||||
public ParserRegex Say { get; set; }
|
public ParserRegex Say { get; set; }
|
||||||
|
public string LocalizeText { get; set; }
|
||||||
public ParserRegex Join { get; set; }
|
public ParserRegex Join { get; set; }
|
||||||
public ParserRegex JoinTeam { get; set; }
|
public ParserRegex JoinTeam { get; set; }
|
||||||
public ParserRegex Quit { get; set; }
|
public ParserRegex Quit { get; set; }
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using IW4MAdmin.Application.Misc;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using IW4MAdmin.Application.Plugin.Script;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
|
|
||||||
|
28
Application/Extensions/ScriptPluginExtensions.cs
Normal file
28
Application/Extensions/ScriptPluginExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Data.Models.Client.Stats;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Extensions;
|
||||||
|
|
||||||
|
public static class ScriptPluginExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<object> GetClientsBasicData(
|
||||||
|
this DbSet<Data.Models.Client.EFClient> set, int[] clientIds)
|
||||||
|
{
|
||||||
|
return set.Where(client => clientIds.Contains(client.ClientId))
|
||||||
|
.Select(client => new
|
||||||
|
{
|
||||||
|
client.ClientId,
|
||||||
|
client.CurrentAlias,
|
||||||
|
client.Level,
|
||||||
|
client.NetworkId
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object> GetClientsStatData(this DbSet<EFClientStatistics> set, int[] clientIds,
|
||||||
|
double serverId)
|
||||||
|
{
|
||||||
|
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Serilog.Core;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
@ -17,7 +18,10 @@ namespace IW4MAdmin.Application.Extensions
|
|||||||
{
|
{
|
||||||
public static class StartupExtensions
|
public static class StartupExtensions
|
||||||
{
|
{
|
||||||
private static ILogger _defaultLogger = null;
|
private static ILogger _defaultLogger;
|
||||||
|
private static readonly LoggingLevelSwitch LevelSwitch = new();
|
||||||
|
private static readonly LoggingLevelSwitch MicrosoftLevelSwitch = new();
|
||||||
|
private static readonly LoggingLevelSwitch SystemLevelSwitch = new();
|
||||||
|
|
||||||
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
|
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
|
||||||
ApplicationConfiguration appConfig)
|
ApplicationConfiguration appConfig)
|
||||||
@ -29,14 +33,21 @@ namespace IW4MAdmin.Application.Extensions
|
|||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var loggerConfig = new LoggerConfiguration()
|
var loggerConfig = new LoggerConfiguration()
|
||||||
.ReadFrom.Configuration(configuration)
|
.ReadFrom.Configuration(configuration);
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
|
|
||||||
|
LevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Default"]);
|
||||||
|
MicrosoftLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:Microsoft"]);
|
||||||
|
SystemLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:System"]);
|
||||||
|
|
||||||
|
loggerConfig = loggerConfig.MinimumLevel.ControlledBy(LevelSwitch);
|
||||||
|
loggerConfig = loggerConfig.MinimumLevel.Override("Microsoft", MicrosoftLevelSwitch)
|
||||||
|
.MinimumLevel.Override("System", SystemLevelSwitch);
|
||||||
|
|
||||||
if (Utilities.IsDevelopment)
|
if (Utilities.IsDevelopment)
|
||||||
{
|
{
|
||||||
loggerConfig = loggerConfig.WriteTo.Console(
|
loggerConfig = loggerConfig.WriteTo.Console(
|
||||||
outputTemplate:
|
outputTemplate:
|
||||||
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
"[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
.MinimumLevel.Debug();
|
.MinimumLevel.Debug();
|
||||||
}
|
}
|
||||||
@ -44,6 +55,15 @@ namespace IW4MAdmin.Application.Extensions
|
|||||||
_defaultLogger = loggerConfig.CreateLogger();
|
_defaultLogger = loggerConfig.CreateLogger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services.AddSingleton((string context) =>
|
||||||
|
{
|
||||||
|
return context.ToLower() switch
|
||||||
|
{
|
||||||
|
"microsoft" => MicrosoftLevelSwitch,
|
||||||
|
"system" => SystemLevelSwitch,
|
||||||
|
_ => LevelSwitch
|
||||||
|
};
|
||||||
|
});
|
||||||
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
|
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
|
||||||
services.AddSingleton(new LoggerFactory()
|
services.AddSingleton(new LoggerFactory()
|
||||||
.AddSerilog(_defaultLogger, true));
|
.AddSerilog(_defaultLogger, true));
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
using IW4MAdmin.Application.Misc;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models;
|
||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
|
using IW4MAdmin.Application.Plugin.Script;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -31,16 +31,11 @@ namespace IW4MAdmin.Application.Factories
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
|
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
|
||||||
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction, Server.Game[] supportedGames)
|
bool isTargetRequired, IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, IEnumerable<Reference.Game> supportedGames)
|
||||||
{
|
{
|
||||||
var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
|
var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
|
||||||
var argsArray = args.Select(_arg => new CommandArgument
|
|
||||||
{
|
|
||||||
Name = _arg.Item1,
|
|
||||||
Required = _arg.Item2
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
|
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, args, executeAction,
|
||||||
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
|
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
using IW4MAdmin.Application.Misc;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Events;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
|
||||||
{
|
|
||||||
public class GameEventHandler : IEventHandler
|
|
||||||
{
|
|
||||||
private readonly EventLog _eventLog;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IEventPublisher _eventPublisher;
|
|
||||||
private static readonly GameEvent.EventType[] overrideEvents = new[]
|
|
||||||
{
|
|
||||||
GameEvent.EventType.Connect,
|
|
||||||
GameEvent.EventType.Disconnect,
|
|
||||||
GameEvent.EventType.Quit,
|
|
||||||
GameEvent.EventType.Stop
|
|
||||||
};
|
|
||||||
|
|
||||||
public GameEventHandler(ILogger<GameEventHandler> logger, IEventPublisher eventPublisher)
|
|
||||||
{
|
|
||||||
_eventLog = new EventLog();
|
|
||||||
_logger = logger;
|
|
||||||
_eventPublisher = eventPublisher;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void HandleEvent(IManager manager, GameEvent gameEvent)
|
|
||||||
{
|
|
||||||
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
|
|
||||||
{
|
|
||||||
EventApi.OnGameEvent(gameEvent);
|
|
||||||
_eventPublisher.Publish(gameEvent);
|
|
||||||
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Skipping event as we're shutting down {eventId}", gameEvent.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
213
Application/IO/BaseConfigurationHandlerV2.cs
Normal file
213
Application/IO/BaseConfigurationHandlerV2.cs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.IO;
|
||||||
|
|
||||||
|
public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHandlerV2<TConfigurationType>
|
||||||
|
where TConfigurationType : class
|
||||||
|
{
|
||||||
|
private readonly ILogger<BaseConfigurationHandlerV2<TConfigurationType>> _logger;
|
||||||
|
private readonly ConfigurationWatcher _watcher;
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new JsonStringEnumConverter()
|
||||||
|
},
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _onIo = new(1, 1);
|
||||||
|
private TConfigurationType _configurationInstance;
|
||||||
|
private string _path = string.Empty;
|
||||||
|
private event Action<string> FileUpdated;
|
||||||
|
|
||||||
|
public BaseConfigurationHandlerV2(ILogger<BaseConfigurationHandlerV2<TConfigurationType>> logger,
|
||||||
|
ConfigurationWatcher watcher)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_watcher = watcher;
|
||||||
|
FileUpdated += OnFileUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
~BaseConfigurationHandlerV2()
|
||||||
|
{
|
||||||
|
FileUpdated -= OnFileUpdated;
|
||||||
|
_watcher.Unregister(_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TConfigurationType> Get(string configurationName,
|
||||||
|
TConfigurationType defaultConfiguration = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(configurationName))
|
||||||
|
{
|
||||||
|
return defaultConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleanName = configurationName.Replace("\\", "").Replace("/", "");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(configurationName))
|
||||||
|
{
|
||||||
|
return defaultConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
_path = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{cleanName}.json");
|
||||||
|
TConfigurationType readConfiguration = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _onIo.WaitAsync();
|
||||||
|
await using var fileStream = File.OpenRead(_path);
|
||||||
|
readConfiguration =
|
||||||
|
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
|
||||||
|
await fileStream.DisposeAsync();
|
||||||
|
_watcher.Register(_path, FileUpdated);
|
||||||
|
|
||||||
|
if (readConfiguration is null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Could not parse configuration {Type} at {FileName}", typeof(TConfigurationType).Name,
|
||||||
|
_path);
|
||||||
|
|
||||||
|
return defaultConfiguration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
if (defaultConfiguration is not null)
|
||||||
|
{
|
||||||
|
await InternalSet(defaultConfiguration, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not read configuration file at {Path}", _path);
|
||||||
|
return defaultConfiguration;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onIo.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onIo.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _configurationInstance ??= readConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Set(TConfigurationType configuration)
|
||||||
|
{
|
||||||
|
await InternalSet(configuration, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Set()
|
||||||
|
{
|
||||||
|
if (_configurationInstance is not null)
|
||||||
|
{
|
||||||
|
await InternalSet(_configurationInstance, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Action<TConfigurationType> Updated;
|
||||||
|
|
||||||
|
private async Task InternalSet(TConfigurationType configuration, bool awaitSemaphore)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (awaitSemaphore)
|
||||||
|
{
|
||||||
|
await _onIo.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var fileStream = File.Create(_path);
|
||||||
|
await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions);
|
||||||
|
await fileStream.DisposeAsync();
|
||||||
|
_configurationInstance = configuration;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not save configuration {Type} {Path}", configuration.GetType().Name, _path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (awaitSemaphore && _onIo.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onIo.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnFileUpdated(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _onIo.WaitAsync();
|
||||||
|
await using var fileStream = File.OpenRead(_path);
|
||||||
|
var readConfiguration =
|
||||||
|
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
|
||||||
|
await fileStream.DisposeAsync();
|
||||||
|
|
||||||
|
if (readConfiguration is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Could not parse updated configuration {Type} at {Path}",
|
||||||
|
typeof(TConfigurationType).Name, filePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CopyUpdatedProperties(readConfiguration);
|
||||||
|
Updated?.Invoke(readConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not parse updated configuration {Type} at {Path}",
|
||||||
|
typeof(TConfigurationType).Name, filePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onIo.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onIo.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyUpdatedProperties(TConfigurationType newConfiguration)
|
||||||
|
{
|
||||||
|
if (_configurationInstance is null)
|
||||||
|
{
|
||||||
|
_configurationInstance = newConfiguration;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Updating existing config with new values {Type} at {Path}", typeof(TConfigurationType).Name,
|
||||||
|
_path);
|
||||||
|
|
||||||
|
if (_configurationInstance is IDictionary configDict && newConfiguration is IDictionary newConfigDict)
|
||||||
|
{
|
||||||
|
configDict.Clear();
|
||||||
|
foreach (var key in newConfigDict.Keys)
|
||||||
|
{
|
||||||
|
configDict.Add(key, newConfigDict[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var property in _configurationInstance.GetType().GetProperties()
|
||||||
|
.Where(prop => prop.CanRead && prop.CanWrite))
|
||||||
|
{
|
||||||
|
property.SetValue(_configurationInstance, property.GetValue(newConfiguration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
Application/IO/ConfigurationWatcher.cs
Normal file
60
Application/IO/ConfigurationWatcher.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.IO;
|
||||||
|
|
||||||
|
public sealed class ConfigurationWatcher : IDisposable
|
||||||
|
{
|
||||||
|
private readonly FileSystemWatcher _watcher;
|
||||||
|
private readonly Dictionary<string, Action<string>> _registeredActions = new();
|
||||||
|
|
||||||
|
public ConfigurationWatcher()
|
||||||
|
{
|
||||||
|
_watcher = new FileSystemWatcher
|
||||||
|
{
|
||||||
|
Path = Path.Join(Utilities.OperatingDirectory, "Configuration"),
|
||||||
|
Filter = "*.json",
|
||||||
|
NotifyFilter = NotifyFilters.LastWrite
|
||||||
|
};
|
||||||
|
|
||||||
|
_watcher.Changed += WatcherOnChanged;
|
||||||
|
_watcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_watcher.Changed -= WatcherOnChanged;
|
||||||
|
_watcher.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(string fileName, Action<string> fileUpdated)
|
||||||
|
{
|
||||||
|
if (_registeredActions.ContainsKey(fileName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredActions.Add(fileName, fileUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister(string fileName)
|
||||||
|
{
|
||||||
|
if (_registeredActions.ContainsKey(fileName))
|
||||||
|
{
|
||||||
|
_registeredActions.Remove(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WatcherOnChanged(object sender, FileSystemEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (!_registeredActions.ContainsKey(eventArgs.FullPath) || eventArgs.ChangeType != WatcherChangeTypes.Changed ||
|
||||||
|
new FileInfo(eventArgs.FullPath).Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -24,8 +25,15 @@ using Serilog.Context;
|
|||||||
using static SharedLibraryCore.Database.Models.EFClient;
|
using static SharedLibraryCore.Database.Models.EFClient;
|
||||||
using Data.Models;
|
using Data.Models;
|
||||||
using Data.Models.Server;
|
using Data.Models.Server;
|
||||||
|
using IW4MAdmin.Application.Alerts;
|
||||||
using IW4MAdmin.Application.Commands;
|
using IW4MAdmin.Application.Commands;
|
||||||
|
using IW4MAdmin.Application.Plugin.Script;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedLibraryCore.Alerts;
|
||||||
|
using SharedLibraryCore.Events.Management;
|
||||||
|
using SharedLibraryCore.Events.Server;
|
||||||
|
using SharedLibraryCore.Interfaces.Events;
|
||||||
using static Data.Models.Client.EFClient;
|
using static Data.Models.Client.EFClient;
|
||||||
|
|
||||||
namespace IW4MAdmin
|
namespace IW4MAdmin
|
||||||
@ -39,11 +47,13 @@ namespace IW4MAdmin
|
|||||||
private const int REPORT_FLAG_COUNT = 4;
|
private const int REPORT_FLAG_COUNT = 4;
|
||||||
private long lastGameTime = 0;
|
private long lastGameTime = 0;
|
||||||
|
|
||||||
public int Id { get; private set; }
|
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IClientNoticeMessageFormatter _messageFormatter;
|
private readonly IClientNoticeMessageFormatter _messageFormatter;
|
||||||
private readonly ILookupCache<EFServer> _serverCache;
|
private readonly ILookupCache<EFServer> _serverCache;
|
||||||
private readonly CommandConfiguration _commandConfiguration;
|
private readonly CommandConfiguration _commandConfiguration;
|
||||||
|
private EFServer _cachedDatabaseServer;
|
||||||
|
private readonly StatManager _statManager;
|
||||||
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
|
||||||
public IW4MServer(
|
public IW4MServer(
|
||||||
ServerConfiguration serverConfiguration,
|
ServerConfiguration serverConfiguration,
|
||||||
@ -67,13 +77,26 @@ namespace IW4MAdmin
|
|||||||
_messageFormatter = messageFormatter;
|
_messageFormatter = messageFormatter;
|
||||||
_serverCache = serverCache;
|
_serverCache = serverCache;
|
||||||
_commandConfiguration = commandConfiguration;
|
_commandConfiguration = commandConfiguration;
|
||||||
|
_statManager = serviceProvider.GetRequiredService<StatManager>();
|
||||||
|
_appConfig = serviceProvider.GetService<ApplicationConfiguration>();
|
||||||
|
|
||||||
|
IGameServerEventSubscriptions.MonitoringStarted += async (gameEvent, token) =>
|
||||||
|
{
|
||||||
|
if (gameEvent.Server.Id != Id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureServerAdded();
|
||||||
|
await _statManager.EnsureServerAdded(gameEvent.Server, token);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
|
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
|
||||||
{
|
{
|
||||||
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
|
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
|
||||||
|
|
||||||
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
|
var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
|
||||||
|
|
||||||
// first time client is connecting to server
|
// first time client is connecting to server
|
||||||
if (client == null)
|
if (client == null)
|
||||||
@ -103,7 +126,7 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
Clients[client.ClientNumber] = client;
|
Clients[client.ClientNumber] = client;
|
||||||
ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
|
ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
|
||||||
var e = new GameEvent()
|
var e = new GameEvent
|
||||||
{
|
{
|
||||||
Origin = client,
|
Origin = client,
|
||||||
Owner = this,
|
Owner = this,
|
||||||
@ -111,12 +134,17 @@ namespace IW4MAdmin
|
|||||||
};
|
};
|
||||||
|
|
||||||
Manager.AddEvent(e);
|
Manager.AddEvent(e);
|
||||||
|
Manager.QueueEvent(new ClientStateInitializeEvent
|
||||||
|
{
|
||||||
|
Client = client,
|
||||||
|
Source = this,
|
||||||
|
});
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnClientDisconnected(EFClient client)
|
public override async Task OnClientDisconnected(EFClient client)
|
||||||
{
|
{
|
||||||
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
|
if (GetClientsAsList().All(eachClient => eachClient.NetworkId != client.NetworkId))
|
||||||
{
|
{
|
||||||
using (LogContext.PushProperty("Server", ToString()))
|
using (LogContext.PushProperty("Server", ToString()))
|
||||||
{
|
{
|
||||||
@ -152,11 +180,9 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
if (E.IsBlocking)
|
if (E.IsBlocking)
|
||||||
{
|
{
|
||||||
await E.Origin?.Lock();
|
await E.Origin.Lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canExecuteCommand = true;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!await ProcessEvent(E))
|
if (!await ProcessEvent(E))
|
||||||
@ -164,54 +190,60 @@ namespace IW4MAdmin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Command C = null;
|
Command command = null;
|
||||||
if (E.Type == GameEvent.EventType.Command)
|
if (E.Type == GameEvent.EventType.Command)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
|
command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (CommandException e)
|
catch (CommandException e)
|
||||||
{
|
{
|
||||||
ServerLogger.LogWarning(e, "Error validating command from event {@event}",
|
ServerLogger.LogWarning(e, "Error validating command from event {@Event}",
|
||||||
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
|
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
|
||||||
E.FailReason = GameEvent.EventFailReason.Invalid;
|
E.FailReason = GameEvent.EventFailReason.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (C != null)
|
if (command != null)
|
||||||
{
|
{
|
||||||
E.Extra = C;
|
E.Extra = command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var canExecuteCommand = Manager.CommandInterceptors.All(interceptor =>
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
|
return interceptor(E);
|
||||||
|
}
|
||||||
if (loginPlugin != null)
|
catch
|
||||||
{
|
{
|
||||||
await loginPlugin.OnEventAsync(E, this);
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
catch (AuthorizationException e)
|
if (!canExecuteCommand)
|
||||||
{
|
{
|
||||||
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
E.Origin.Tell(_translationLookup["SERVER_COMMANDS_INTERCEPTED"]);
|
||||||
canExecuteCommand = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// hack: this prevents commands from getting executing that 'shouldn't' be
|
else if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd)
|
||||||
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
|
|
||||||
(canExecuteCommand || E.Origin?.Level == Permission.Console))
|
|
||||||
{
|
{
|
||||||
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
|
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
|
||||||
await command.ExecuteAsync(E);
|
E.Origin.ToString());
|
||||||
|
await cmd.ExecuteAsync(E);
|
||||||
|
Manager.QueueEvent(new ClientExecuteCommandEvent
|
||||||
|
{
|
||||||
|
Command = cmd,
|
||||||
|
Client = E.Origin,
|
||||||
|
Source = this,
|
||||||
|
CommandText = E.Data
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var pluginTasks = Manager.Plugins
|
var pluginTasks = Manager.Plugins.Where(plugin => !plugin.IsParser)
|
||||||
.Where(_plugin => _plugin.Name != "Login")
|
.Select(plugin => CreatePluginTask(plugin, E));
|
||||||
.Select(async plugin => await CreatePluginTask(plugin, E));
|
|
||||||
|
|
||||||
await Task.WhenAll(pluginTasks);
|
await Task.WhenAll(pluginTasks);
|
||||||
}
|
}
|
||||||
@ -248,7 +280,7 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
|
await plugin.OnEventAsync(gameEvent, this);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@ -275,29 +307,7 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
ServerLogger.LogDebug("processing event of type {type}", E.Type);
|
ServerLogger.LogDebug("processing event of type {type}", E.Type);
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Start)
|
if (E.Type == GameEvent.EventType.ConnectionLost)
|
||||||
{
|
|
||||||
var existingServer = (await _serverCache
|
|
||||||
.FirstAsync(server => server.Id == EndPoint));
|
|
||||||
|
|
||||||
var serverId = await GetIdForServer(E.Owner);
|
|
||||||
|
|
||||||
if (existingServer == null)
|
|
||||||
{
|
|
||||||
var server = new EFServer()
|
|
||||||
{
|
|
||||||
Port = Port,
|
|
||||||
EndPoint = ToString(),
|
|
||||||
ServerId = serverId,
|
|
||||||
GameName = (Reference.Game?)GameName,
|
|
||||||
HostName = Hostname
|
|
||||||
};
|
|
||||||
|
|
||||||
await _serverCache.AddAsync(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.ConnectionLost)
|
|
||||||
{
|
{
|
||||||
var exception = E.Extra as Exception;
|
var exception = E.Extra as Exception;
|
||||||
ServerLogger.LogError(exception,
|
ServerLogger.LogError(exception,
|
||||||
@ -305,7 +315,15 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||||
{
|
{
|
||||||
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
|
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{ListenAddress}:{ListenPort}"));
|
||||||
|
|
||||||
|
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||||
|
.WithCategory(Alert.AlertCategory.Error)
|
||||||
|
.FromSource("System")
|
||||||
|
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{ListenAddress}:{ListenPort}"))
|
||||||
|
.ExpiresIn(TimeSpan.FromDays(1));
|
||||||
|
|
||||||
|
Manager.AlertManager.AddAlert(alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
Throttled = true;
|
Throttled = true;
|
||||||
@ -314,11 +332,19 @@ namespace IW4MAdmin
|
|||||||
else if (E.Type == GameEvent.EventType.ConnectionRestored)
|
else if (E.Type == GameEvent.EventType.ConnectionRestored)
|
||||||
{
|
{
|
||||||
ServerLogger.LogInformation(
|
ServerLogger.LogInformation(
|
||||||
"Connection restored with {server}", ToString());
|
"Connection restored with {Server}", ToString());
|
||||||
|
|
||||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||||
{
|
{
|
||||||
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
|
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{ListenAddress}:{ListenPort}"));
|
||||||
|
|
||||||
|
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||||
|
.WithCategory(Alert.AlertCategory.Information)
|
||||||
|
.FromSource("System")
|
||||||
|
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{ListenAddress}:{ListenPort}"))
|
||||||
|
.ExpiresIn(TimeSpan.FromDays(1));
|
||||||
|
|
||||||
|
Manager.AlertManager.AddAlert(alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(CustomSayName))
|
if (!string.IsNullOrEmpty(CustomSayName))
|
||||||
@ -332,9 +358,18 @@ namespace IW4MAdmin
|
|||||||
else if (E.Type == GameEvent.EventType.ChangePermission)
|
else if (E.Type == GameEvent.EventType.ChangePermission)
|
||||||
{
|
{
|
||||||
var newPermission = (Permission) E.Extra;
|
var newPermission = (Permission) E.Extra;
|
||||||
|
var oldPermission = E.Target.Level;
|
||||||
ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}",
|
ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}",
|
||||||
E.Origin.ToString(), E.Target.ToString(), newPermission);
|
E.Origin.ToString(), E.Target.ToString(), newPermission);
|
||||||
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
|
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPermissionChangeEvent
|
||||||
|
{
|
||||||
|
Client = E.Origin,
|
||||||
|
Source = this,
|
||||||
|
OldPermission = oldPermission,
|
||||||
|
NewPermission = newPermission
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Connect)
|
else if (E.Type == GameEvent.EventType.Connect)
|
||||||
@ -342,7 +377,6 @@ namespace IW4MAdmin
|
|||||||
if (E.Origin.State != ClientState.Connected)
|
if (E.Origin.State != ClientState.Connected)
|
||||||
{
|
{
|
||||||
E.Origin.State = ClientState.Connected;
|
E.Origin.State = ClientState.Connected;
|
||||||
E.Origin.LastConnection = DateTime.UtcNow;
|
|
||||||
E.Origin.Connections += 1;
|
E.Origin.Connections += 1;
|
||||||
|
|
||||||
ChatHistory.Add(new ChatInfo()
|
ChatHistory.Add(new ChatInfo()
|
||||||
@ -355,9 +389,9 @@ namespace IW4MAdmin
|
|||||||
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
|
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
|
||||||
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken);
|
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken);
|
||||||
|
|
||||||
if (clientTag?.LinkedMeta != null)
|
if (clientTag?.Value != null)
|
||||||
{
|
{
|
||||||
E.Origin.Tag = clientTag.LinkedMeta.Value;
|
E.Origin.Tag = clientTag.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -431,6 +465,7 @@ namespace IW4MAdmin
|
|||||||
Clients[E.Origin.ClientNumber] = E.Origin;
|
Clients[E.Origin.ClientNumber] = E.Origin;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
E.Origin.GameName = (Reference.Game)GameName;
|
||||||
E.Origin = await OnClientConnected(E.Origin);
|
E.Origin = await OnClientConnected(E.Origin);
|
||||||
E.Target = E.Origin;
|
E.Target = E.Origin;
|
||||||
}
|
}
|
||||||
@ -481,6 +516,12 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
await Manager.GetPenaltyService().Create(newPenalty);
|
await Manager.GetPenaltyService().Create(newPenalty);
|
||||||
E.Target.SetLevel(Permission.Flagged, E.Origin);
|
E.Target.SetLevel(Permission.Flagged, E.Origin);
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyEvent
|
||||||
|
{
|
||||||
|
Client = E.Target,
|
||||||
|
Penalty = newPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Unflag)
|
else if (E.Type == GameEvent.EventType.Unflag)
|
||||||
@ -498,8 +539,14 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
E.Target.SetLevel(Permission.User, E.Origin);
|
E.Target.SetLevel(Permission.User, E.Origin);
|
||||||
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
|
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
|
||||||
E.Target.CurrentAlias?.IPAddress);
|
E.Target.GameName, E.Target.CurrentAlias?.IPAddress, new[] {EFPenalty.PenaltyType.Flag});
|
||||||
await Manager.GetPenaltyService().Create(unflagPenalty);
|
await Manager.GetPenaltyService().Create(unflagPenalty);
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyRevokeEvent
|
||||||
|
{
|
||||||
|
Client = E.Target,
|
||||||
|
Penalty = unflagPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Report)
|
else if (E.Type == GameEvent.EventType.Report)
|
||||||
@ -535,6 +582,13 @@ namespace IW4MAdmin
|
|||||||
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
|
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
|
||||||
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
|
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyEvent
|
||||||
|
{
|
||||||
|
Client = E.Target,
|
||||||
|
Penalty = newReport,
|
||||||
|
Source = this
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.TempBan)
|
else if (E.Type == GameEvent.EventType.TempBan)
|
||||||
@ -572,10 +626,8 @@ namespace IW4MAdmin
|
|||||||
Time = DateTime.UtcNow
|
Time = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
|
|
||||||
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId,
|
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId);
|
||||||
Manager.CancellationToken);
|
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId);
|
||||||
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId,
|
|
||||||
Manager.CancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.PreDisconnect)
|
else if (E.Type == GameEvent.EventType.PreDisconnect)
|
||||||
@ -658,7 +710,7 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.MapChange)
|
else if (E.Type == GameEvent.EventType.MapChange)
|
||||||
{
|
{
|
||||||
ServerLogger.LogInformation("New map loaded - {clientCount} active players", ClientNum);
|
ServerLogger.LogInformation("New map loaded - {ClientCount} active players", ClientNum);
|
||||||
|
|
||||||
// iw4 doesn't log the game info
|
// iw4 doesn't log the game info
|
||||||
if (E.Extra == null)
|
if (E.Extra == null)
|
||||||
@ -672,29 +724,64 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Gametype = dict["gametype"];
|
if (dict.ContainsKey("gametype"))
|
||||||
Hostname = dict["hostname"];
|
{
|
||||||
|
UpdateGametype(dict["gametype"]);
|
||||||
|
}
|
||||||
|
|
||||||
string mapname = dict["mapname"] ?? CurrentMap.Name;
|
if (dict.ContainsKey("hostname"))
|
||||||
UpdateMap(mapname);
|
{
|
||||||
|
UpdateHostname(dict["hostname"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newMapName = dict.ContainsKey("mapname")
|
||||||
|
? dict["mapname"] ?? CurrentMap.Name
|
||||||
|
: CurrentMap.Name;
|
||||||
|
UpdateMap(newMapName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var dict = (Dictionary<string, string>)E.Extra;
|
var dict = (Dictionary<string, string>)E.Extra;
|
||||||
Gametype = dict["g_gametype"];
|
|
||||||
Hostname = dict["sv_hostname"];
|
|
||||||
MaxClients = int.Parse(dict["sv_maxclients"]);
|
|
||||||
|
|
||||||
string mapname = dict["mapname"];
|
if (dict.ContainsKey("g_gametype"))
|
||||||
UpdateMap(mapname);
|
{
|
||||||
|
UpdateGametype(dict["g_gametype"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dict.ContainsKey("sv_hostname"))
|
||||||
|
{
|
||||||
|
UpdateHostname(dict["sv_hostname"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dict.ContainsKey("sv_maxclients"))
|
||||||
|
{
|
||||||
|
UpdateMaxPlayers(int.Parse(dict["sv_maxclients"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (dict.ContainsKey("com_maxclients"))
|
||||||
|
{
|
||||||
|
UpdateMaxPlayers(int.Parse(dict["com_maxclients"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (dict.ContainsKey("com_maxplayers"))
|
||||||
|
{
|
||||||
|
UpdateMaxPlayers(int.Parse(dict["com_maxplayers"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dict.ContainsKey("mapname"))
|
||||||
|
{
|
||||||
|
UpdateMap(dict["mapname"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.GameTime.HasValue)
|
if (E.GameTime.HasValue)
|
||||||
{
|
{
|
||||||
lastGameTime = E.GameTime.Value;
|
lastGameTime = E.GameTime.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatchStartTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.MapEnd)
|
else if (E.Type == GameEvent.EventType.MapEnd)
|
||||||
@ -705,6 +792,8 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
lastGameTime = E.GameTime.Value;
|
lastGameTime = E.GameTime.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatchEndTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Tell)
|
else if (E.Type == GameEvent.EventType.Tell)
|
||||||
@ -744,9 +833,56 @@ namespace IW4MAdmin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task EnsureServerAdded()
|
||||||
|
{
|
||||||
|
var gameServer = await _serverCache
|
||||||
|
.FirstAsync(server => server.EndPoint == base.Id);
|
||||||
|
|
||||||
|
if (gameServer == null)
|
||||||
|
{
|
||||||
|
gameServer = new EFServer
|
||||||
|
{
|
||||||
|
Port = ListenPort,
|
||||||
|
EndPoint = base.Id,
|
||||||
|
ServerId = BuildLegacyDatabaseId(),
|
||||||
|
GameName = (Reference.Game?)GameName,
|
||||||
|
HostName = ServerName
|
||||||
|
};
|
||||||
|
|
||||||
|
await _serverCache.AddAsync(gameServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var context = _serviceProvider.GetRequiredService<IDatabaseContextFactory>()
|
||||||
|
.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
|
context.Servers.Attach(gameServer);
|
||||||
|
|
||||||
|
// we want to set the gamename up if it's never been set, or it changed
|
||||||
|
if (!gameServer.GameName.HasValue || gameServer.GameName.Value != GameCode)
|
||||||
|
{
|
||||||
|
gameServer.GameName = GameCode;
|
||||||
|
context.Entry(gameServer).Property(property => property.GameName).IsModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameServer.HostName == null || gameServer.HostName != ServerName)
|
||||||
|
{
|
||||||
|
gameServer.HostName = ServerName;
|
||||||
|
context.Entry(gameServer).Property(property => property.HostName).IsModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameServer.IsPasswordProtected != !string.IsNullOrEmpty(GamePassword))
|
||||||
|
{
|
||||||
|
gameServer.IsPasswordProtected = !string.IsNullOrEmpty(GamePassword);
|
||||||
|
context.Entry(gameServer).Property(property => property.IsPasswordProtected).IsModified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
_cachedDatabaseServer = gameServer;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnClientUpdate(EFClient origin)
|
private async Task OnClientUpdate(EFClient origin)
|
||||||
{
|
{
|
||||||
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
|
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
|
||||||
|
|
||||||
if (client == null)
|
if (client == null)
|
||||||
{
|
{
|
||||||
@ -791,10 +927,22 @@ namespace IW4MAdmin
|
|||||||
/// array index 2 = updated clients
|
/// array index 2 = updated clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
async Task<List<EFClient>[]> PollPlayersAsync()
|
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
if (DateTime.Now - (MatchEndTime ?? MatchStartTime) < TimeSpan.FromSeconds(15))
|
||||||
|
{
|
||||||
|
ServerLogger.LogDebug("Skipping status poll attempt because the match ended recently");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var currentClients = GetClientsAsList();
|
var currentClients = GetClientsAsList();
|
||||||
var statusResponse = await this.GetStatusAsync(Manager.CancellationToken);
|
var statusResponse = await this.GetStatusAsync(token);
|
||||||
|
|
||||||
|
if (statusResponse is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var polledClients = statusResponse.Clients.AsEnumerable();
|
var polledClients = statusResponse.Clients.AsEnumerable();
|
||||||
|
|
||||||
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
|
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
|
||||||
@ -822,40 +970,54 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
server ??= this;
|
server ??= this;
|
||||||
|
|
||||||
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
|
return (await _serverCache.FirstAsync(cachedServer =>
|
||||||
{
|
cachedServer.EndPoint == server.Id || cachedServer.ServerId == server.EndPoint)).ServerId;
|
||||||
return 886229536;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: this is not stable and will need to be migrated again...
|
private long BuildLegacyDatabaseId()
|
||||||
long id = HashCode.Combine(server.IP, server.Port);
|
{
|
||||||
id = id < 0 ? Math.Abs(id) : id;
|
long id = HashCode.Combine(ListenAddress, ListenPort);
|
||||||
|
return id < 0 ? Math.Abs(id) : id;
|
||||||
var serverId = (await _serverCache
|
|
||||||
.FirstAsync(_server => _server.ServerId == server.EndPoint ||
|
|
||||||
_server.EndPoint == server.ToString() ||
|
|
||||||
_server.ServerId == id))?.ServerId;
|
|
||||||
|
|
||||||
return !serverId.HasValue ? id : serverId.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateMap(string mapname)
|
private void UpdateMap(string mapName)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(mapname))
|
if (string.IsNullOrEmpty(mapName))
|
||||||
{
|
{
|
||||||
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundMap = Maps.Find(m => m.Name == mapName) ?? new Map
|
||||||
{
|
{
|
||||||
Alias = mapname,
|
Alias = mapName,
|
||||||
Name = mapname
|
Name = mapName
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (foundMap == CurrentMap)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentMap = foundMap;
|
||||||
|
|
||||||
|
using(LogContext.PushProperty("Server", Id))
|
||||||
|
{
|
||||||
|
ServerLogger.LogDebug("Updating map to {@CurrentMap}", CurrentMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateGametype(string gameType)
|
private void UpdateGametype(string gameType)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(gameType))
|
if (string.IsNullOrEmpty(gameType))
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Gametype = gameType;
|
Gametype = gameType;
|
||||||
|
|
||||||
|
using(LogContext.PushProperty("Server", Id))
|
||||||
|
{
|
||||||
|
ServerLogger.LogDebug("Updating gametype to {Gametype}", gameType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -866,7 +1028,7 @@ namespace IW4MAdmin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using(LogContext.PushProperty("Server", ToString()))
|
using(LogContext.PushProperty("Server", Id))
|
||||||
{
|
{
|
||||||
ServerLogger.LogDebug("Updating hostname to {HostName}", hostname);
|
ServerLogger.LogDebug("Updating hostname to {HostName}", hostname);
|
||||||
}
|
}
|
||||||
@ -881,7 +1043,7 @@ namespace IW4MAdmin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using(LogContext.PushProperty("Server", ToString()))
|
using(LogContext.PushProperty("Server", Id))
|
||||||
{
|
{
|
||||||
ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers);
|
ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers);
|
||||||
}
|
}
|
||||||
@ -895,7 +1057,7 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
await client.OnDisconnect();
|
await client.OnDisconnect();
|
||||||
|
|
||||||
var e = new GameEvent()
|
var e = new GameEvent
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Disconnect,
|
Type = GameEvent.EventType.Disconnect,
|
||||||
Owner = this,
|
Owner = this,
|
||||||
@ -906,16 +1068,24 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
|
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using var tokenSource = new CancellationTokenSource();
|
||||||
|
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
|
||||||
|
|
||||||
|
Manager.QueueEvent(new MonitorStopEvent
|
||||||
|
{
|
||||||
|
Server = this
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateTime _lastMessageSent = DateTime.Now;
|
private DateTime _lastMessageSent = DateTime.Now;
|
||||||
private DateTime _lastPlayerCount = DateTime.Now;
|
private DateTime _lastPlayerCount = DateTime.Now;
|
||||||
|
|
||||||
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (cts.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await ShutdownInternal();
|
await ShutdownInternal();
|
||||||
return true;
|
return true;
|
||||||
@ -929,13 +1099,18 @@ namespace IW4MAdmin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var polledClients = await PollPlayersAsync();
|
var polledClients = await PollPlayersAsync(token);
|
||||||
|
|
||||||
|
if (polledClients is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var disconnectingClient in polledClients[1]
|
foreach (var disconnectingClient in polledClients[1]
|
||||||
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
|
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||||
{
|
{
|
||||||
disconnectingClient.CurrentServer = this;
|
disconnectingClient.CurrentServer = this;
|
||||||
var e = new GameEvent()
|
var e = new GameEvent
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.PreDisconnect,
|
Type = GameEvent.EventType.PreDisconnect,
|
||||||
Origin = disconnectingClient,
|
Origin = disconnectingClient,
|
||||||
@ -952,6 +1127,8 @@ namespace IW4MAdmin
|
|||||||
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
|
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
|
||||||
{
|
{
|
||||||
client.CurrentServer = this;
|
client.CurrentServer = this;
|
||||||
|
client.GameName = (Reference.Game)GameName;
|
||||||
|
|
||||||
var e = new GameEvent
|
var e = new GameEvent
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.PreConnect,
|
Type = GameEvent.EventType.PreConnect,
|
||||||
@ -959,7 +1136,7 @@ namespace IW4MAdmin
|
|||||||
Owner = this,
|
Owner = this,
|
||||||
IsBlocking = true,
|
IsBlocking = true,
|
||||||
Extra = client.GetAdditionalProperty<string>("BotGuid"),
|
Extra = client.GetAdditionalProperty<string>("BotGuid"),
|
||||||
Source = GameEvent.EventSource.Status
|
Source = GameEvent.EventSource.Status,
|
||||||
};
|
};
|
||||||
|
|
||||||
Manager.AddEvent(e);
|
Manager.AddEvent(e);
|
||||||
@ -980,6 +1157,16 @@ namespace IW4MAdmin
|
|||||||
Manager.AddEvent(gameEvent);
|
Manager.AddEvent(gameEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (polledClients[2].Any())
|
||||||
|
{
|
||||||
|
Manager.QueueEvent(new ClientDataUpdateEvent
|
||||||
|
{
|
||||||
|
Clients = new ReadOnlyCollection<EFClient>(polledClients[2]),
|
||||||
|
Server = this,
|
||||||
|
Source = this,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (Throttled)
|
if (Throttled)
|
||||||
{
|
{
|
||||||
var gameEvent = new GameEvent
|
var gameEvent = new GameEvent
|
||||||
@ -991,6 +1178,12 @@ namespace IW4MAdmin
|
|||||||
};
|
};
|
||||||
|
|
||||||
Manager.AddEvent(gameEvent);
|
Manager.AddEvent(gameEvent);
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ConnectionRestoreEvent
|
||||||
|
{
|
||||||
|
Server = this,
|
||||||
|
Source = this
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LastPoll = DateTime.Now;
|
LastPoll = DateTime.Now;
|
||||||
@ -1014,6 +1207,12 @@ namespace IW4MAdmin
|
|||||||
};
|
};
|
||||||
|
|
||||||
Manager.AddEvent(gameEvent);
|
Manager.AddEvent(gameEvent);
|
||||||
|
Manager.QueueEvent(new ConnectionInterruptEvent
|
||||||
|
{
|
||||||
|
Server = this,
|
||||||
|
Source = this
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -1064,22 +1263,20 @@ namespace IW4MAdmin
|
|||||||
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
|
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
|
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{ListenAddress}:{ListenPort}]"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RunServerCollection()
|
private void RunServerCollection()
|
||||||
{
|
{
|
||||||
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
|
if (DateTime.Now - _lastPlayerCount < _appConfig?.ServerDataCollectionInterval)
|
||||||
|
|
||||||
if (DateTime.Now - _lastPlayerCount < appConfig?.ServerDataCollectionInterval)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
|
var maxItems = Math.Ceiling(_appConfig!.MaxClientHistoryTime.TotalMinutes /
|
||||||
appConfig.ServerDataCollectionInterval.TotalMinutes);
|
_appConfig.ServerDataCollectionInterval.TotalMinutes);
|
||||||
|
|
||||||
while (ClientHistory.ClientCounts.Count > maxItems)
|
while (ClientHistory.ClientCounts.Count > maxItems)
|
||||||
{
|
{
|
||||||
@ -1103,13 +1300,13 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
ResolvedIpEndPoint =
|
ResolvedIpEndPoint =
|
||||||
new IPEndPoint(
|
new IPEndPoint(
|
||||||
(await Dns.GetHostAddressesAsync(IP)).First(address =>
|
(await Dns.GetHostAddressesAsync(ListenAddress)).First(address =>
|
||||||
address.AddressFamily == AddressFamily.InterNetwork), Port);
|
address.AddressFamily == AddressFamily.InterNetwork), ListenPort);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {IP}:{Port}", IP, Port);
|
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {Address}:{Port}", ListenAddress, ListenPort);
|
||||||
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(IP), Port);
|
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(ListenAddress), ListenPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
RconParser = Manager.AdditionalRConParsers
|
RconParser = Manager.AdditionalRConParsers
|
||||||
@ -1166,10 +1363,12 @@ namespace IW4MAdmin
|
|||||||
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync", token: Manager.CancellationToken);
|
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync", token: Manager.CancellationToken);
|
||||||
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip", token: Manager.CancellationToken);
|
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip", token: Manager.CancellationToken);
|
||||||
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "", token: Manager.CancellationToken);
|
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "", token: Manager.CancellationToken);
|
||||||
|
var privateClients = await this.GetMappedDvarValueOrDefaultAsync("sv_privateClients", overrideDefault: 0,
|
||||||
|
token: Manager.CancellationToken);
|
||||||
|
|
||||||
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
|
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
|
||||||
{
|
{
|
||||||
await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName,
|
await this.SetDvarAsync("sv_sayname", CustomSayName,
|
||||||
Manager.CancellationToken);
|
Manager.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1207,36 +1406,27 @@ namespace IW4MAdmin
|
|||||||
}
|
}
|
||||||
|
|
||||||
WorkingDirectory = basepath.Value;
|
WorkingDirectory = basepath.Value;
|
||||||
this.Hostname = hostname;
|
Hostname = hostname;
|
||||||
this.MaxClients = maxplayers;
|
MaxClients = maxplayers;
|
||||||
this.FSGame = game.Value;
|
FSGame = game.Value;
|
||||||
this.Gametype = gametype;
|
Gametype = gametype;
|
||||||
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
|
IP = ip.Value is "localhost" or "0.0.0.0" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
|
||||||
this.GamePassword = gamePassword.Value;
|
GamePassword = gamePassword.Value;
|
||||||
|
PrivateClientSlots = privateClients.Value;
|
||||||
|
|
||||||
UpdateMap(mapname);
|
UpdateMap(mapname);
|
||||||
|
|
||||||
if (RconParser.CanGenerateLogPath)
|
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
||||||
{
|
{
|
||||||
bool needsRestart = false;
|
|
||||||
|
|
||||||
if (logsync.Value == 0)
|
if (logsync.Value == 0)
|
||||||
{
|
{
|
||||||
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
|
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
|
||||||
needsRestart = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(logfile.Value))
|
if (string.IsNullOrWhiteSpace(logfile.Value))
|
||||||
{
|
{
|
||||||
logfile.Value = "games_mp.log";
|
logfile.Value = "games_mp.log";
|
||||||
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
|
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
|
||||||
needsRestart = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsRestart)
|
|
||||||
{
|
|
||||||
// disabling this for the time being
|
|
||||||
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
|
|
||||||
await this.ExecuteCommandAsync("map_restart");*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this DVAR isn't set until the a map is loaded
|
// this DVAR isn't set until the a map is loaded
|
||||||
@ -1385,6 +1575,12 @@ namespace IW4MAdmin
|
|||||||
.FormatExt(activeClient.Warnings, activeClient.Name, reason);
|
.FormatExt(activeClient.Warnings, activeClient.Name, reason);
|
||||||
activeClient.CurrentServer.Broadcast(message);
|
activeClient.CurrentServer.Broadcast(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyEvent
|
||||||
|
{
|
||||||
|
Client = targetClient,
|
||||||
|
Penalty = newPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
|
public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
|
||||||
@ -1423,8 +1619,20 @@ namespace IW4MAdmin
|
|||||||
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
|
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
|
||||||
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
|
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyEvent
|
||||||
|
{
|
||||||
|
Client = targetClient,
|
||||||
|
Penalty = newPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task<string[]> ExecuteCommandAsync(string command, CancellationToken token = default) =>
|
||||||
|
Utilities.ExecuteCommandAsync(this, command, token);
|
||||||
|
|
||||||
|
public override Task SetDvarAsync(string name, object value, CancellationToken token = default) =>
|
||||||
|
Utilities.SetDvarAsync(this, name, value, token);
|
||||||
|
|
||||||
public override async Task TempBan(string reason, TimeSpan length, EFClient targetClient, EFClient originClient)
|
public override async Task TempBan(string reason, TimeSpan length, EFClient targetClient, EFClient originClient)
|
||||||
{
|
{
|
||||||
// ensure player gets kicked if command not performed on them in the same server
|
// ensure player gets kicked if command not performed on them in the same server
|
||||||
@ -1443,6 +1651,11 @@ namespace IW4MAdmin
|
|||||||
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
|
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
|
||||||
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
|
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
|
||||||
|
|
||||||
|
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
|
||||||
|
{
|
||||||
|
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
|
||||||
|
}
|
||||||
|
|
||||||
if (activeClient.IsIngame)
|
if (activeClient.IsIngame)
|
||||||
{
|
{
|
||||||
var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
|
var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
|
||||||
@ -1451,6 +1664,12 @@ namespace IW4MAdmin
|
|||||||
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
|
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
|
||||||
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
|
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyEvent
|
||||||
|
{
|
||||||
|
Client = targetClient,
|
||||||
|
Penalty = newPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
|
public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
|
||||||
@ -1472,6 +1691,11 @@ namespace IW4MAdmin
|
|||||||
activeClient.SetLevel(Permission.Banned, originClient);
|
activeClient.SetLevel(Permission.Banned, originClient);
|
||||||
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
|
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
|
||||||
|
|
||||||
|
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
|
||||||
|
{
|
||||||
|
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
|
||||||
|
}
|
||||||
|
|
||||||
if (activeClient.IsIngame)
|
if (activeClient.IsIngame)
|
||||||
{
|
{
|
||||||
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());
|
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());
|
||||||
@ -1481,6 +1705,12 @@ namespace IW4MAdmin
|
|||||||
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
|
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
|
||||||
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
|
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyEvent
|
||||||
|
{
|
||||||
|
Client = targetClient,
|
||||||
|
Penalty = newPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
|
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
|
||||||
@ -1500,8 +1730,14 @@ namespace IW4MAdmin
|
|||||||
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
|
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
|
||||||
targetClient.SetLevel(Permission.User, originClient);
|
targetClient.SetLevel(Permission.User, originClient);
|
||||||
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
|
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
|
||||||
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress);
|
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
|
||||||
await Manager.GetPenaltyService().Create(unbanPenalty);
|
await Manager.GetPenaltyService().Create(unbanPenalty);
|
||||||
|
|
||||||
|
Manager.QueueEvent(new ClientPenaltyRevokeEvent
|
||||||
|
{
|
||||||
|
Client = targetClient,
|
||||||
|
Penalty = unbanPenalty
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void InitializeTokens()
|
public override void InitializeTokens()
|
||||||
@ -1511,5 +1747,7 @@ namespace IW4MAdmin
|
|||||||
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
|
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
|
||||||
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
|
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override long LegacyDatabaseId => _cachedDatabaseServer.ServerId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,16 +27,23 @@ using System.Threading.Tasks;
|
|||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
using Data.Helpers;
|
using Data.Helpers;
|
||||||
using Integrations.Source.Extensions;
|
using Integrations.Source.Extensions;
|
||||||
|
using IW4MAdmin.Application.Alerts;
|
||||||
using IW4MAdmin.Application.Extensions;
|
using IW4MAdmin.Application.Extensions;
|
||||||
|
using IW4MAdmin.Application.IO;
|
||||||
using IW4MAdmin.Application.Localization;
|
using IW4MAdmin.Application.Localization;
|
||||||
|
using IW4MAdmin.Application.Plugin;
|
||||||
|
using IW4MAdmin.Application.Plugin.Script;
|
||||||
|
using IW4MAdmin.Application.QueryHelpers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
|
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
|
||||||
using IW4MAdmin.Plugins.Stats.Client;
|
using IW4MAdmin.Plugins.Stats.Client;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Stats.Client.Abstractions;
|
using Stats.Client.Abstractions;
|
||||||
using Stats.Client;
|
using Stats.Client;
|
||||||
using Stats.Config;
|
using Stats.Config;
|
||||||
using Stats.Helpers;
|
using Stats.Helpers;
|
||||||
|
using WebfrontCore.QueryHelpers.Models;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
@ -45,15 +52,36 @@ namespace IW4MAdmin.Application
|
|||||||
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
|
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
|
||||||
private static ApplicationManager _serverManager;
|
private static ApplicationManager _serverManager;
|
||||||
private static Task _applicationTask;
|
private static Task _applicationTask;
|
||||||
private static ServiceProvider _serviceProvider;
|
private static IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// entrypoint of the application
|
/// entrypoint of the application
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 25, int? requestQueueLimit = 25)
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
|
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
|
||||||
|
{
|
||||||
|
var libraryName = eventArgs.Name.Split(",").First();
|
||||||
|
|
||||||
|
var overrides = new[] { nameof(SharedLibraryCore), nameof(Stats) };
|
||||||
|
if (!overrides.Contains(libraryName))
|
||||||
|
{
|
||||||
|
return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(asm => asm.FullName == eventArgs.Name);
|
||||||
|
}
|
||||||
|
// added to be a bit more permissive with plugin references
|
||||||
|
return AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(asm => asm.FullName?.StartsWith(libraryName) ?? false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (noConfirm)
|
||||||
|
{
|
||||||
|
AppContext.SetSwitch("NoConfirmPrompt", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Environment.SetEnvironmentVariable("MaxConcurrentRequests", (maxConcurrentRequests * Environment.ProcessorCount).ToString());
|
||||||
|
Environment.SetEnvironmentVariable("RequestQueueLimit", requestQueueLimit.ToString());
|
||||||
|
|
||||||
Console.OutputEncoding = Encoding.UTF8;
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
@ -66,7 +94,7 @@ namespace IW4MAdmin.Application
|
|||||||
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
|
|
||||||
await LaunchAsync(args);
|
await LaunchAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -92,13 +120,13 @@ namespace IW4MAdmin.Application
|
|||||||
/// task that initializes application and starts the application monitoring and runtime tasks
|
/// task that initializes application and starts the application monitoring and runtime tasks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private static async Task LaunchAsync(string[] args)
|
private static async Task LaunchAsync()
|
||||||
{
|
{
|
||||||
restart:
|
restart:
|
||||||
ITranslationLookup translationLookup = null;
|
ITranslationLookup translationLookup = null;
|
||||||
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
|
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
|
||||||
Utilities.DefaultLogger = logger;
|
Utilities.DefaultLogger = logger;
|
||||||
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
|
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version}", Version);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -106,23 +134,23 @@ namespace IW4MAdmin.Application
|
|||||||
ConfigurationMigration.MoveConfigFolder10518(null);
|
ConfigurationMigration.MoveConfigFolder10518(null);
|
||||||
ConfigurationMigration.CheckDirectories();
|
ConfigurationMigration.CheckDirectories();
|
||||||
ConfigurationMigration.RemoveObsoletePlugins20210322();
|
ConfigurationMigration.RemoveObsoletePlugins20210322();
|
||||||
|
|
||||||
logger.LogDebug("Configuring services...");
|
logger.LogDebug("Configuring services...");
|
||||||
var services = await ConfigureServices(args);
|
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
var configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||||
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
|
await configHandler.BuildAsync();
|
||||||
|
_serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
|
||||||
|
(configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
|
||||||
|
|
||||||
_serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
|
_serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
|
||||||
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
|
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
|
||||||
|
|
||||||
_applicationTask = RunApplicationTasksAsync(logger, services);
|
|
||||||
var tasks = new[]
|
|
||||||
{
|
|
||||||
versionChecker.CheckVersion(),
|
|
||||||
_applicationTask
|
|
||||||
};
|
|
||||||
|
|
||||||
await _serverManager.Init();
|
await _serverManager.Init();
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
_applicationTask = RunApplicationTasksAsync(logger, _serverManager, _serviceProvider);
|
||||||
|
|
||||||
|
await _applicationTask;
|
||||||
|
logger.LogInformation("Shutdown completed successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -172,21 +200,55 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
goto restart;
|
goto restart;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _serviceProvider.DisposeAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// runs the core application tasks
|
/// runs the core application tasks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
|
private static Task RunApplicationTasksAsync(ILogger logger, ApplicationManager applicationManager,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
|
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>();
|
||||||
|
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
|
||||||
|
var masterCommunicator = serviceProvider.GetRequiredService<IMasterCommunication>();
|
||||||
|
var webfrontLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
|
||||||
|
using var onWebfrontErrored = new ManualResetEventSlim();
|
||||||
|
|
||||||
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
|
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
|
||||||
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
|
? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken).ContinueWith(continuation =>
|
||||||
|
{
|
||||||
|
if (!continuation.IsFaulted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogCritical("Unable to start webfront task. {Message}",
|
||||||
|
continuation.Exception?.InnerException?.Message);
|
||||||
|
|
||||||
|
logger.LogDebug(continuation.Exception, "Unable to start webfront task");
|
||||||
|
|
||||||
|
onWebfrontErrored.Set();
|
||||||
|
|
||||||
|
})
|
||||||
: Task.CompletedTask;
|
: Task.CompletedTask;
|
||||||
|
|
||||||
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
|
if (_serverManager.GetApplicationSettings().Configuration().EnableWebFront)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
onWebfrontErrored.Wait(webfrontLifetime.ApplicationStarted);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored when webfront successfully starts
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onWebfrontErrored.IsSet)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// we want to run this one on a manual thread instead of letting the thread pool handle it,
|
// 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
|
// because we can't exit early from waiting on console input, and it prevents us from restarting
|
||||||
@ -197,18 +259,15 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
var tasks = new[]
|
var tasks = new[]
|
||||||
{
|
{
|
||||||
|
applicationManager.Start(),
|
||||||
|
versionChecker.CheckVersion(),
|
||||||
webfrontTask,
|
webfrontTask,
|
||||||
_serverManager.Start(),
|
masterCommunicator.RunUploadStatus(_serverManager.CancellationToken),
|
||||||
_serviceProvider.GetRequiredService<IMasterCommunication>()
|
|
||||||
.RunUploadStatus(_serverManager.CancellationToken),
|
|
||||||
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
|
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.LogDebug("Starting webfront and input tasks");
|
logger.LogDebug("Starting webfront and input tasks");
|
||||||
await Task.WhenAll(tasks);
|
return Task.WhenAll(tasks);
|
||||||
|
|
||||||
logger.LogInformation("Shutdown completed successfully");
|
|
||||||
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -296,8 +355,21 @@ namespace IW4MAdmin.Application
|
|||||||
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
|
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
|
||||||
foreach (var pluginType in plugins)
|
foreach (var pluginType in plugins)
|
||||||
{
|
{
|
||||||
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
|
var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null;
|
||||||
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
|
|
||||||
|
defaultLogger.LogDebug("Registering plugin type {Name}", pluginType.FullName);
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton(!isV2 ? typeof(IPlugin) : typeof(IPluginV2), pluginType);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var registrationMethod = pluginType.GetMethod(nameof(IPluginV2.RegisterDependencies));
|
||||||
|
registrationMethod?.Invoke(null, new object[] { serviceCollection });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
defaultLogger.LogError(ex, "Could not register plugin of type {Type}", pluginType.Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// register the plugin commands
|
// register the plugin commands
|
||||||
@ -318,10 +390,13 @@ namespace IW4MAdmin.Application
|
|||||||
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
|
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// register any script plugins
|
var scriptPlugins = pluginImporter.DiscoverScriptPlugins();
|
||||||
foreach (var plugin in pluginImporter.DiscoverScriptPlugins())
|
|
||||||
|
foreach (var scriptPlugin in scriptPlugins)
|
||||||
{
|
{
|
||||||
serviceCollection.AddSingleton(plugin);
|
serviceCollection.AddSingleton(scriptPlugin.Item1, sp =>
|
||||||
|
sp.GetRequiredService<IScriptPluginFactory>()
|
||||||
|
.CreateScriptPlugin(scriptPlugin.Item1, scriptPlugin.Item2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// register any eventable types
|
// register any eventable types
|
||||||
@ -342,22 +417,27 @@ namespace IW4MAdmin.Application
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the dependency injection services
|
/// Configures the dependency injection services
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<IServiceCollection> ConfigureServices(string[] args)
|
private static void ConfigureServices(IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
// todo: this is a quick fix
|
// todo: this is a quick fix
|
||||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
|
|
||||||
// setup the static resources (config/master api/translations)
|
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
|
||||||
var serviceCollection = new ServiceCollection();
|
.AddConfiguration<DefaultSettings>()
|
||||||
|
.AddConfiguration<CommandConfiguration>()
|
||||||
|
.AddConfiguration<StatsConfiguration>("StatsPluginSettings");
|
||||||
|
|
||||||
|
// for legacy purposes. update at some point
|
||||||
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||||
await appConfigHandler.BuildAsync();
|
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
|
||||||
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
|
|
||||||
await defaultConfigHandler.BuildAsync();
|
|
||||||
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
|
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
|
||||||
await commandConfigHandler.BuildAsync();
|
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
|
||||||
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
|
||||||
await statsCommandHandler.BuildAsync();
|
if (appConfigHandler.Configuration()?.MasterUrl == new Uri("http://api.raidmax.org:5000"))
|
||||||
var defaultConfig = defaultConfigHandler.Configuration();
|
{
|
||||||
|
appConfigHandler.Configuration().MasterUrl = new ApplicationConfiguration().MasterUrl;
|
||||||
|
}
|
||||||
|
|
||||||
var appConfig = appConfigHandler.Configuration();
|
var appConfig = appConfigHandler.Configuration();
|
||||||
var masterUri = Utilities.IsDevelopment
|
var masterUri = Utilities.IsDevelopment
|
||||||
? new Uri("http://127.0.0.1:8080")
|
? new Uri("http://127.0.0.1:8080")
|
||||||
@ -374,7 +454,7 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
|
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
|
||||||
appConfigHandler.Set(appConfig);
|
appConfigHandler.Set(appConfig);
|
||||||
await appConfigHandler.Save();
|
appConfigHandler.Save().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
// register override level names
|
// register override level names
|
||||||
@ -387,17 +467,10 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
// build the dependency list
|
// build the dependency list
|
||||||
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
|
|
||||||
|
|
||||||
serviceCollection
|
serviceCollection
|
||||||
.AddBaseLogger(appConfig)
|
.AddBaseLogger(appConfig)
|
||||||
.AddSingleton(defaultConfig)
|
|
||||||
.AddSingleton<IServiceCollection>(serviceCollection)
|
|
||||||
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
|
|
||||||
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
|
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
|
||||||
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
|
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
|
||||||
.AddSingleton(appConfig)
|
|
||||||
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
|
|
||||||
.AddSingleton(serviceProvider =>
|
.AddSingleton(serviceProvider =>
|
||||||
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
|
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
|
||||||
.Configuration() ?? new CommandConfiguration())
|
.Configuration() ?? new CommandConfiguration())
|
||||||
@ -430,6 +503,7 @@ namespace IW4MAdmin.Application
|
|||||||
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
|
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
|
||||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
|
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
|
||||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
|
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
|
||||||
|
.AddSingleton<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>, ClientResourceQueryHelper>()
|
||||||
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
|
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
|
||||||
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
|
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
|
||||||
.AddSingleton<IMasterCommunication, MasterCommunication>()
|
.AddSingleton<IMasterCommunication, MasterCommunication>()
|
||||||
@ -446,24 +520,22 @@ namespace IW4MAdmin.Application
|
|||||||
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
|
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
|
||||||
.AddSingleton<IServerDataViewer, ServerDataViewer>()
|
.AddSingleton<IServerDataViewer, ServerDataViewer>()
|
||||||
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
||||||
.AddSingleton<IEventPublisher, EventPublisher>()
|
|
||||||
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
||||||
|
.AddSingleton<IAlertManager, AlertManager>()
|
||||||
|
#pragma warning disable CS0618
|
||||||
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
|
||||||
|
.AddSingleton<IRemoteCommandService, RemoteCommandService>()
|
||||||
|
.AddSingleton(new ConfigurationWatcher())
|
||||||
|
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
|
||||||
|
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
|
||||||
.AddSingleton(translationLookup)
|
.AddSingleton(translationLookup)
|
||||||
.AddDatabaseContextOptions(appConfig);
|
.AddDatabaseContextOptions(appConfig);
|
||||||
|
|
||||||
if (args.Contains("serialevents"))
|
serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>();
|
||||||
{
|
|
||||||
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceCollection.AddSource();
|
serviceCollection.AddSource();
|
||||||
|
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
|
||||||
return serviceCollection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
|
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
|
||||||
|
@ -27,7 +27,8 @@ public class
|
|||||||
await using var context = _contextFactory.CreateContext();
|
await using var context = _contextFactory.CreateContext();
|
||||||
|
|
||||||
var auditEntries = context.EFChangeHistory.Where(change => change.TargetEntityId == query.ClientId)
|
var auditEntries = context.EFChangeHistory.Where(change => change.TargetEntityId == query.ClientId)
|
||||||
.Where(change => change.TypeOfChange == EFChangeHistory.ChangeType.Permission);
|
.Where(change => change.TypeOfChange == EFChangeHistory.ChangeType.Permission)
|
||||||
|
.OrderByDescending(change => change.TimeChanged);
|
||||||
|
|
||||||
var audits = from change in auditEntries
|
var audits = from change in auditEntries
|
||||||
join client in context.Clients
|
join client in context.Clients
|
||||||
|
12
Application/Misc/AsyncResult.cs
Normal file
12
Application/Misc/AsyncResult.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc;
|
||||||
|
|
||||||
|
public class AsyncResult : IAsyncResult
|
||||||
|
{
|
||||||
|
public object AsyncState { get; set; }
|
||||||
|
public WaitHandle AsyncWaitHandle { get; set; }
|
||||||
|
public bool CompletedSynchronously { get; set; }
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
}
|
@ -49,8 +49,10 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await _onSaving.WaitAsync();
|
||||||
await using var fileStream = File.OpenRead(FileName);
|
await using var fileStream = File.OpenRead(FileName);
|
||||||
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
|
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
|
||||||
|
await fileStream.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
@ -66,6 +68,13 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
ConfigurationFileName = FileName
|
ConfigurationFileName = FileName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onSaving.CurrentCount == 0)
|
||||||
|
{
|
||||||
|
_onSaving.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Save()
|
public async Task Save()
|
||||||
@ -76,6 +85,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
|
|
||||||
await using var fileStream = File.Create(FileName);
|
await using var fileStream = File.Create(FileName);
|
||||||
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
|
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
|
||||||
|
await fileStream.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
finally
|
finally
|
||||||
|
@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
builder.Append(header);
|
builder.Append(header);
|
||||||
builder.Append(config.NoticeLineSeparator);
|
builder.Append(config.NoticeLineSeparator);
|
||||||
// build the reason
|
// build the reason
|
||||||
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
|
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
|
||||||
|
|
||||||
if (isNewLineSeparator)
|
if (isNewLineSeparator)
|
||||||
{
|
{
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
|
||||||
{
|
|
||||||
public class EventLog : Dictionary<long, IList<GameEvent>>
|
|
||||||
{
|
|
||||||
private static JsonSerializerSettings serializationSettings;
|
|
||||||
|
|
||||||
public static JsonSerializerSettings BuildVcrSerializationSettings()
|
|
||||||
{
|
|
||||||
if (serializationSettings == null)
|
|
||||||
{
|
|
||||||
serializationSettings = new JsonSerializerSettings() { Formatting = Formatting.Indented, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
|
|
||||||
serializationSettings.Converters.Add(new IPAddressConverter());
|
|
||||||
serializationSettings.Converters.Add(new IPEndPointConverter());
|
|
||||||
serializationSettings.Converters.Add(new GameEventConverter());
|
|
||||||
serializationSettings.Converters.Add(new ClientEntityConverter());
|
|
||||||
}
|
|
||||||
|
|
||||||
return serializationSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
|
||||||
{
|
|
||||||
public class EventPublisher : IEventPublisher
|
|
||||||
{
|
|
||||||
public event EventHandler<GameEvent> OnClientDisconnect;
|
|
||||||
public event EventHandler<GameEvent> OnClientConnect;
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
public EventPublisher(ILogger<EventPublisher> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Publish(GameEvent gameEvent)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Handling publishing event of type {EventType}", gameEvent.Type);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (gameEvent.Type == GameEvent.EventType.Connect)
|
|
||||||
{
|
|
||||||
OnClientConnect?.Invoke(this, gameEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameEvent.Type == GameEvent.EventType.Disconnect)
|
|
||||||
{
|
|
||||||
OnClientDisconnect?.Invoke(this, gameEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Could not publish event of type {EventType}", gameEvent.Type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,7 +22,7 @@ public class GeoLocationService : IGeoLocationService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new DatabaseReader(_sourceAddress);
|
using var reader = new DatabaseReader(_sourceAddress);
|
||||||
reader.TryCountry(address, out country);
|
country = reader.Country(address);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
171
Application/Misc/InteractionRegistration.cs
Normal file
171
Application/Misc/InteractionRegistration.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models;
|
||||||
|
using IW4MAdmin.Application.Plugin.Script;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using InteractionRegistrationCallback =
|
||||||
|
System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken,
|
||||||
|
System.Threading.Tasks.Task<SharedLibraryCore.Interfaces.IInteractionData>>;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc;
|
||||||
|
|
||||||
|
public class InteractionRegistration : IInteractionRegistration
|
||||||
|
{
|
||||||
|
private readonly ILogger<InteractionRegistration> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ConcurrentDictionary<string, InteractionRegistrationCallback> _interactions = new();
|
||||||
|
|
||||||
|
public InteractionRegistration(ILogger<InteractionRegistration> logger, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(source))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Script interaction source cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Registering script interaction {InteractionName} from {Source}", interactionName, source);
|
||||||
|
|
||||||
|
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
|
||||||
|
.FirstOrDefault(plugin => plugin.Name == source);
|
||||||
|
|
||||||
|
if (plugin is not ScriptPlugin scriptPlugin)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<IInteractionData> WrappedDelegate(int? clientId, Reference.Game? game, CancellationToken token) =>
|
||||||
|
Task.FromResult(
|
||||||
|
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, token, clientId, game, token));
|
||||||
|
|
||||||
|
if (!_interactions.ContainsKey(interactionName))
|
||||||
|
{
|
||||||
|
_interactions.TryAdd(interactionName, WrappedDelegate);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_interactions[interactionName] = WrappedDelegate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterInteraction(string interactionName, InteractionRegistrationCallback interactionRegistration)
|
||||||
|
{
|
||||||
|
if (!_interactions.ContainsKey(interactionName))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Registering interaction {InteractionName}", interactionName);
|
||||||
|
_interactions.TryAdd(interactionName, interactionRegistration);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Updating interaction {InteractionName}", interactionName);
|
||||||
|
_interactions[interactionName] = interactionRegistration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnregisterInteraction(string interactionName)
|
||||||
|
{
|
||||||
|
if (!_interactions.ContainsKey(interactionName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Unregistering interaction {InteractionName}", interactionName);
|
||||||
|
_interactions.TryRemove(interactionName, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null,
|
||||||
|
int? clientId = null,
|
||||||
|
Reference.Game? game = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return await GetInteractionsInternal(interactionPrefix, clientId, game, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null,
|
||||||
|
Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!_interactions.ContainsKey(interactionId))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Interaction with ID {interactionId} has not been registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var interaction = await _interactions[interactionId](targetId, game, token);
|
||||||
|
|
||||||
|
if (interaction.Action is not null)
|
||||||
|
{
|
||||||
|
return await interaction.Action(originId, targetId, game, meta, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.ScriptAction is not null)
|
||||||
|
{
|
||||||
|
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>())
|
||||||
|
{
|
||||||
|
if (plugin is not ScriptPlugin scriptPlugin || scriptPlugin.Name != interaction.Source)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, token, originId, targetId, game, meta,
|
||||||
|
token);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPluginV2>>())
|
||||||
|
{
|
||||||
|
if (plugin is not ScriptPluginV2 scriptPlugin || scriptPlugin.Name != interaction.Source)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scriptPlugin
|
||||||
|
.QueryWithErrorHandling(interaction.ScriptAction, originId, targetId, game, meta, token)
|
||||||
|
?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Could not process interaction for {InteractionName} and OriginId {ClientId}",
|
||||||
|
interactionId, originId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<IInteractionData>> GetInteractionsInternal(string prefix = null,
|
||||||
|
int? clientId = null, Reference.Game? game = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var interactions = _interactions
|
||||||
|
.Where(interaction => string.IsNullOrWhiteSpace(prefix) || interaction.Key.StartsWith(prefix)).Select(
|
||||||
|
async kvp =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await kvp.Value(clientId, game, token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,
|
||||||
|
"Could not get interaction for {InteractionName} and ClientId {ClientId}",
|
||||||
|
kvp.Key,
|
||||||
|
clientId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await Task.WhenAll(interactions))
|
||||||
|
.Where(interaction => interaction is not null)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
@ -103,17 +103,15 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
await UploadStatus();
|
await UploadStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Could not send heartbeat");
|
_logger.LogWarning("Could not send heartbeat - {Message}", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(Interval, token);
|
await Task.Delay(Interval, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@ -149,13 +147,13 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
Map = s.CurrentMap.Name,
|
Map = s.CurrentMap.Name,
|
||||||
MaxClientNum = s.MaxClients,
|
MaxClientNum = s.MaxClients,
|
||||||
Id = s.EndPoint,
|
Id = s.EndPoint,
|
||||||
Port = (short)s.Port,
|
Port = (short)s.ListenPort,
|
||||||
IPAddress = s.IP
|
IPAddress = s.ListenAddress
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
WebfrontUrl = _appConfig.WebfrontUrl
|
WebfrontUrl = _appConfig.WebfrontUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
Response<ResultMessage> response = null;
|
Response<ResultMessage> response;
|
||||||
|
|
||||||
if (_firstHeartBeat)
|
if (_firstHeartBeat)
|
||||||
{
|
{
|
||||||
@ -170,7 +168,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
|
|
||||||
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
|
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Non success response code from master is {statusCode}, message is {message}", response.ResponseMessage.StatusCode, response.StringContent);
|
_logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.ResponseMessage.StatusCode, response.StringContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ public class MetaServiceV2 : IMetaServiceV2
|
|||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory)
|
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_metaActions = new Dictionary<MetaType, List<dynamic>>();
|
_metaActions = new Dictionary<MetaType, List<dynamic>>();
|
||||||
|
@ -1,177 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using System.Linq;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using IW4MAdmin.Application.API.Master;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using SharedLibraryCore.Configuration;
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// implementation of IPluginImporter
|
|
||||||
/// discovers plugins and script plugins
|
|
||||||
/// </summary>
|
|
||||||
public class PluginImporter : IPluginImporter
|
|
||||||
{
|
|
||||||
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
|
|
||||||
private static readonly string PLUGIN_DIR = "Plugins";
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
|
|
||||||
private readonly IMasterApi _masterApi;
|
|
||||||
private readonly ApplicationConfiguration _appConfig;
|
|
||||||
|
|
||||||
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_masterApi = masterApi;
|
|
||||||
_remoteAssemblyHandler = remoteAssemblyHandler;
|
|
||||||
_appConfig = appConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// discovers all the script plugins in the plugins dir
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<IPlugin> DiscoverScriptPlugins()
|
|
||||||
{
|
|
||||||
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
|
|
||||||
|
|
||||||
if (!Directory.Exists(pluginDir))
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<IPlugin>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var scriptPluginFiles =
|
|
||||||
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
|
|
||||||
|
|
||||||
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count);
|
|
||||||
|
|
||||||
return scriptPluginFiles.Select(fileName =>
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
|
|
||||||
return new ScriptPlugin(_logger, fileName);
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// discovers all the C# assembly plugins and commands
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
|
|
||||||
{
|
|
||||||
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
|
|
||||||
var pluginTypes = Enumerable.Empty<Type>();
|
|
||||||
var commandTypes = Enumerable.Empty<Type>();
|
|
||||||
var configurationTypes = Enumerable.Empty<Type>();
|
|
||||||
|
|
||||||
if (Directory.Exists(pluginDir))
|
|
||||||
{
|
|
||||||
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
|
|
||||||
_logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length);
|
|
||||||
|
|
||||||
if (dllFileNames.Length > 0)
|
|
||||||
{
|
|
||||||
// we only want to load the most recent assembly in case of duplicates
|
|
||||||
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name))
|
|
||||||
.Union(GetRemoteAssemblies())
|
|
||||||
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
|
|
||||||
|
|
||||||
pluginTypes = assemblies
|
|
||||||
.SelectMany(_asm =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _asm.GetTypes();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<Type>();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
|
|
||||||
|
|
||||||
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
|
|
||||||
|
|
||||||
commandTypes = assemblies
|
|
||||||
.SelectMany(_asm =>{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _asm.GetTypes();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<Type>();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
|
|
||||||
|
|
||||||
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
|
|
||||||
|
|
||||||
configurationTypes = assemblies
|
|
||||||
.SelectMany(asm => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return asm.GetTypes();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<Type>();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Where(asmType =>
|
|
||||||
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);
|
|
||||||
|
|
||||||
_logger.LogDebug("Discovered {count} configuration implementations", configurationTypes.Count());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (pluginTypes, commandTypes, configurationTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Assembly> GetRemoteAssemblies()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_pluginSubscription == null)
|
|
||||||
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
|
|
||||||
|
|
||||||
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Could not load remote assemblies");
|
|
||||||
return Enumerable.Empty<Assembly>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<string> GetRemoteScripts()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_pluginSubscription == null)
|
|
||||||
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
|
|
||||||
|
|
||||||
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex,"Could not load remote scripts");
|
|
||||||
return Enumerable.Empty<string>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PluginType
|
|
||||||
{
|
|
||||||
Binary,
|
|
||||||
Script
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,10 +13,10 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
public class RemoteAssemblyHandler : IRemoteAssemblyHandler
|
public class RemoteAssemblyHandler : IRemoteAssemblyHandler
|
||||||
{
|
{
|
||||||
private const int keyLength = 32;
|
private const int KeyLength = 32;
|
||||||
private const int tagLength = 16;
|
private const int TagLength = 16;
|
||||||
private const int nonceLength = 12;
|
private const int NonceLength = 12;
|
||||||
private const int iterationCount = 10000;
|
private const int IterationCount = 10000;
|
||||||
|
|
||||||
private readonly ApplicationConfiguration _appconfig;
|
private readonly ApplicationConfiguration _appconfig;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
@ -30,7 +30,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
|
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
|
||||||
{
|
{
|
||||||
return DecryptContent(encryptedAssemblies)
|
return DecryptContent(encryptedAssemblies)
|
||||||
.Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
|
.Select(Assembly.Load);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
|
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
|
||||||
@ -38,24 +38,24 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
|
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[][] DecryptContent(string[] content)
|
private IEnumerable<byte[]> DecryptContent(string[] content)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
|
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
|
||||||
{
|
{
|
||||||
_logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
|
_logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
|
||||||
return new byte[0][];
|
return Array.Empty<byte[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var assemblies = content.Select(piece =>
|
var assemblies = content.Select(piece =>
|
||||||
{
|
{
|
||||||
byte[] byteContent = Convert.FromBase64String(piece);
|
var byteContent = Convert.FromBase64String(piece);
|
||||||
byte[] encryptedContent = byteContent.Take(byteContent.Length - (tagLength + nonceLength)).ToArray();
|
var encryptedContent = byteContent.Take(byteContent.Length - (TagLength + NonceLength)).ToArray();
|
||||||
byte[] tag = byteContent.Skip(byteContent.Length - (tagLength + nonceLength)).Take(tagLength).ToArray();
|
var tag = byteContent.Skip(byteContent.Length - (TagLength + NonceLength)).Take(TagLength).ToArray();
|
||||||
byte[] nonce = byteContent.Skip(byteContent.Length - nonceLength).Take(nonceLength).ToArray();
|
var nonce = byteContent.Skip(byteContent.Length - NonceLength).Take(NonceLength).ToArray();
|
||||||
byte[] decryptedContent = new byte[encryptedContent.Length];
|
var decryptedContent = new byte[encryptedContent.Length];
|
||||||
|
|
||||||
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id.ToString()), iterationCount, HashAlgorithmName.SHA512);
|
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512);
|
||||||
var encryption = new AesGcm(keyGen.GetBytes(keyLength));
|
var encryption = new AesGcm(keyGen.GetBytes(KeyLength));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
109
Application/Misc/RemoteCommandService.cs
Normal file
109
Application/Misc/RemoteCommandService.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.Services;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc;
|
||||||
|
|
||||||
|
public class RemoteCommandService : IRemoteCommandService
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
private readonly ClientService _clientService;
|
||||||
|
|
||||||
|
public RemoteCommandService(ILogger<RemoteCommandService> logger, ApplicationConfiguration appConfig, ClientService clientService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_appConfig = appConfig;
|
||||||
|
_clientService = clientService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
|
||||||
|
IEnumerable<string> arguments, Server server)
|
||||||
|
{
|
||||||
|
var (_, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool, IEnumerable<CommandResponseInfo>)> ExecuteWithResult(int originId, int? targetId, string command,
|
||||||
|
IEnumerable<string> arguments, Server server)
|
||||||
|
{
|
||||||
|
if (originId < 1)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Not executing command {Command} for {Originid} because origin id is invalid", command,
|
||||||
|
originId);
|
||||||
|
return (false, Enumerable.Empty<CommandResponseInfo>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = await _clientService.Get(originId);
|
||||||
|
client.CurrentServer = server;
|
||||||
|
|
||||||
|
command += $" {(targetId.HasValue ? $"@{targetId} " : "")}{string.Join(" ", arguments ?? Enumerable.Empty<string>())}";
|
||||||
|
|
||||||
|
var remoteEvent = new GameEvent
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Command,
|
||||||
|
Data = command.StartsWith(_appConfig.CommandPrefix) ||
|
||||||
|
command.StartsWith(_appConfig.BroadcastCommandPrefix)
|
||||||
|
? command
|
||||||
|
: $"{_appConfig.CommandPrefix}{command}",
|
||||||
|
Origin = client,
|
||||||
|
Owner = server,
|
||||||
|
IsRemote = true,
|
||||||
|
CorrelationId = Guid.NewGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
server.Manager.AddEvent(remoteEvent);
|
||||||
|
CommandResponseInfo[] response;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// wait for the event to process
|
||||||
|
var completedEvent =
|
||||||
|
await remoteEvent.WaitAsync(Utilities.DefaultCommandTimeout, server.Manager.CancellationToken);
|
||||||
|
|
||||||
|
if (completedEvent.FailReason == GameEvent.EventFailReason.Timeout)
|
||||||
|
{
|
||||||
|
response = new[]
|
||||||
|
{
|
||||||
|
new CommandResponseInfo
|
||||||
|
{
|
||||||
|
ClientId = client.ClientId,
|
||||||
|
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response = completedEvent.Output.Select(output => new CommandResponseInfo()
|
||||||
|
{
|
||||||
|
Response = output,
|
||||||
|
ClientId = client.ClientId
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
response = new[]
|
||||||
|
{
|
||||||
|
new CommandResponseInfo
|
||||||
|
{
|
||||||
|
ClientId = client.ClientId,
|
||||||
|
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (!remoteEvent.Failed, response);
|
||||||
|
}
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using IW4MAdmin.Application.Configuration;
|
|
||||||
using Jint;
|
|
||||||
using Jint.Native;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
|
||||||
{
|
|
||||||
public class ScriptPluginConfigurationWrapper
|
|
||||||
{
|
|
||||||
private readonly BaseConfigurationHandler<ScriptPluginConfiguration> _handler;
|
|
||||||
private ScriptPluginConfiguration _config;
|
|
||||||
private readonly string _pluginName;
|
|
||||||
private readonly Engine _scriptEngine;
|
|
||||||
|
|
||||||
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
|
|
||||||
{
|
|
||||||
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
|
|
||||||
_pluginName = pluginName;
|
|
||||||
_scriptEngine = scriptEngine;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
|
||||||
{
|
|
||||||
await _handler.BuildAsync();
|
|
||||||
_config = _handler.Configuration() ??
|
|
||||||
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int? AsInteger(double d)
|
|
||||||
{
|
|
||||||
return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetValue(string key, object value)
|
|
||||||
{
|
|
||||||
var castValue = value;
|
|
||||||
|
|
||||||
if (value is double d)
|
|
||||||
{
|
|
||||||
castValue = AsInteger(d) ?? value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
|
|
||||||
{
|
|
||||||
castValue = array.Select(item => AsInteger((double)item)).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_config.ContainsKey(_pluginName))
|
|
||||||
{
|
|
||||||
_config.Add(_pluginName, new Dictionary<string, object>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var plugin = _config[_pluginName];
|
|
||||||
|
|
||||||
if (plugin.ContainsKey(key))
|
|
||||||
{
|
|
||||||
plugin[key] = castValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
plugin.Add(key, castValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
_handler.Set(_config);
|
|
||||||
await _handler.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsValue GetValue(string key)
|
|
||||||
{
|
|
||||||
if (!_config.ContainsKey(_pluginName))
|
|
||||||
{
|
|
||||||
return JsValue.Undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_config[_pluginName].ContainsKey(key))
|
|
||||||
{
|
|
||||||
return JsValue.Undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = _config[_pluginName][key];
|
|
||||||
|
|
||||||
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
|
|
||||||
{
|
|
||||||
item = jElem.Deserialize<List<dynamic>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsValue.FromObject(_scriptEngine, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,8 +12,10 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Events.Management;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.Interfaces.Events;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
namespace IW4MAdmin.Application.Misc
|
||||||
{
|
{
|
||||||
@ -24,28 +26,20 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
private readonly IManager _manager;
|
private readonly IManager _manager;
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private readonly ApplicationConfiguration _appConfig;
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
private readonly IEventPublisher _eventPublisher;
|
|
||||||
|
|
||||||
private bool _inProgress;
|
private bool _inProgress;
|
||||||
private TimeSpan _period;
|
private TimeSpan _period;
|
||||||
|
|
||||||
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
|
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
|
||||||
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
|
IManager manager, IDatabaseContextFactory contextFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appConfig = appConfig;
|
_appConfig = appConfig;
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_eventPublisher = eventPublisher;
|
|
||||||
|
|
||||||
_eventPublisher.OnClientConnect += SaveConnectionInfo;
|
IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
|
||||||
_eventPublisher.OnClientDisconnect += SaveConnectionInfo;
|
IManagementEventSubscriptions.ClientStateDisposed += SaveConnectionInfo;
|
||||||
}
|
|
||||||
|
|
||||||
~ServerDataCollector()
|
|
||||||
{
|
|
||||||
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
|
|
||||||
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
|
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
|
||||||
@ -131,18 +125,19 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
await context.SaveChangesAsync(token);
|
await context.SaveChangesAsync(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveConnectionInfo(object sender, GameEvent gameEvent)
|
private async Task SaveConnectionInfo(ClientStateEvent stateEvent, CancellationToken token)
|
||||||
{
|
{
|
||||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
context.ConnectionHistory.Add(new EFClientConnectionHistory
|
context.ConnectionHistory.Add(new EFClientConnectionHistory
|
||||||
{
|
{
|
||||||
ClientId = gameEvent.Origin.ClientId,
|
ClientId = stateEvent.Client.ClientId,
|
||||||
ServerId = gameEvent.Owner.GetIdForServer().Result,
|
ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
|
||||||
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
|
ConnectionType = stateEvent is ClientStateAuthorizeEvent
|
||||||
? Reference.ConnectionType.Connect
|
? Reference.ConnectionType.Connect
|
||||||
: Reference.ConnectionType.Disconnect
|
: Reference.ConnectionType.Disconnect
|
||||||
});
|
});
|
||||||
context.SaveChanges();
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,9 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
|
using Data.Models;
|
||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
|
using Data.Models.Client.Stats;
|
||||||
using Data.Models.Server;
|
using Data.Models.Server;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -22,26 +24,37 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
|
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
|
||||||
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
||||||
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
|
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
|
||||||
|
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
|
||||||
|
|
||||||
private readonly TimeSpan? _cacheTimeSpan =
|
private readonly TimeSpan? _cacheTimeSpan =
|
||||||
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
|
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
|
||||||
|
|
||||||
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
|
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
|
||||||
IDataValueCache<EFClient, (int, int)> serverStatsCache,
|
IDataValueCache<EFClient, (int, int)> serverStatsCache,
|
||||||
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
|
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_snapshotCache = snapshotCache;
|
_snapshotCache = snapshotCache;
|
||||||
_serverStatsCache = serverStatsCache;
|
_serverStatsCache = serverStatsCache;
|
||||||
_clientHistoryCache = clientHistoryCache;
|
_clientHistoryCache = clientHistoryCache;
|
||||||
|
_rankedClientsCache = rankedClientsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(int?, DateTime?)>
|
public async Task<(int?, DateTime?)>
|
||||||
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
|
MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
|
_snapshotCache.SetCacheItem(async (snapshots, ids, cancellationToken) =>
|
||||||
{
|
{
|
||||||
|
Reference.Game? game = null;
|
||||||
|
long? id = null;
|
||||||
|
|
||||||
|
if (ids.Any())
|
||||||
|
{
|
||||||
|
game = (Reference.Game?)ids.First();
|
||||||
|
id = (long?)ids.Last();
|
||||||
|
}
|
||||||
|
|
||||||
var oldestEntry = overPeriod.HasValue
|
var oldestEntry = overPeriod.HasValue
|
||||||
? DateTime.UtcNow - overPeriod.Value
|
? DateTime.UtcNow - overPeriod.Value
|
||||||
: DateTime.UtcNow.AddDays(-1);
|
: DateTime.UtcNow.AddDays(-1);
|
||||||
@ -49,9 +62,10 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
int? maxClients;
|
int? maxClients;
|
||||||
DateTime? maxClientsTime;
|
DateTime? maxClientsTime;
|
||||||
|
|
||||||
if (serverId != null)
|
if (id != null)
|
||||||
{
|
{
|
||||||
var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
|
var clients = await snapshots.Where(snapshot => snapshot.ServerId == id)
|
||||||
|
.Where(snapshot => game == null || snapshot.Server.GameName == game)
|
||||||
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
|
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
|
||||||
.OrderByDescending(snapshot => snapshot.ClientCount)
|
.OrderByDescending(snapshot => snapshot.ClientCount)
|
||||||
.Select(snapshot => new
|
.Select(snapshot => new
|
||||||
@ -68,6 +82,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
|
var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
|
||||||
|
.Where(snapshot => game == null || snapshot.Server.GameName == game)
|
||||||
.GroupBy(snapshot => snapshot.PeriodBlock)
|
.GroupBy(snapshot => snapshot.PeriodBlock)
|
||||||
.Select(grp => new
|
.Select(grp => new
|
||||||
{
|
{
|
||||||
@ -84,11 +99,12 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
|
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
|
||||||
|
|
||||||
return (maxClients, maxClientsTime);
|
return (maxClients, maxClientsTime);
|
||||||
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true);
|
}, nameof(MaxConcurrentClientsAsync), new object[] { gameCode, serverId }, _cacheTimeSpan, true);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
|
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync),
|
||||||
|
new object[] { gameCode, serverId }, token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -97,22 +113,30 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
|
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
_serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
|
_serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) =>
|
||||||
{
|
{
|
||||||
var count = await set.CountAsync(cancellationToken);
|
Reference.Game? game = null;
|
||||||
|
|
||||||
|
if (ids.Any())
|
||||||
|
{
|
||||||
|
game = (Reference.Game?)ids.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = await set.CountAsync(item => game == null || item.GameName == game,
|
||||||
|
cancellationToken);
|
||||||
var startOfPeriod =
|
var startOfPeriod =
|
||||||
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
|
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
|
||||||
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
|
var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return (count, recentCount);
|
return (count, recentCount);
|
||||||
}, nameof(_serverStatsCache), _cacheTimeSpan, true);
|
}, nameof(_serverStatsCache), new object[] { gameCode }, _cacheTimeSpan, true);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
|
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), new object[] { gameCode }, token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -160,5 +184,37 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return Enumerable.Empty<ClientHistoryInfo>();
|
return Enumerable.Empty<ClientHistoryInfo>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
_rankedClientsCache.SetCacheItem((set, ids, cancellationToken) =>
|
||||||
|
{
|
||||||
|
long? id = null;
|
||||||
|
|
||||||
|
if (ids.Any())
|
||||||
|
{
|
||||||
|
id = (long?)ids.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||||
|
return set
|
||||||
|
.Where(rating => rating.Newest)
|
||||||
|
.Where(rating => rating.ServerId == id)
|
||||||
|
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
|
||||||
|
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
|
||||||
|
.Where(rating => rating.Ranking != null)
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
}, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,40 +9,41 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
internal class TokenAuthentication : ITokenAuthentication
|
internal class TokenAuthentication : ITokenAuthentication
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<long, TokenState> _tokens;
|
private readonly ConcurrentDictionary<int, TokenState> _tokens;
|
||||||
private readonly RandomNumberGenerator _random;
|
private readonly RandomNumberGenerator _random;
|
||||||
private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120);
|
private static readonly TimeSpan TimeoutPeriod = new(0, 0, 120);
|
||||||
private const short TokenLength = 4;
|
private const short TokenLength = 4;
|
||||||
|
|
||||||
public TokenAuthentication()
|
public TokenAuthentication()
|
||||||
{
|
{
|
||||||
_tokens = new ConcurrentDictionary<long, TokenState>();
|
_tokens = new ConcurrentDictionary<int, TokenState>();
|
||||||
_random = RandomNumberGenerator.Create();
|
_random = RandomNumberGenerator.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AuthorizeToken(long networkId, string token)
|
public bool AuthorizeToken(ITokenIdentifier authInfo)
|
||||||
{
|
{
|
||||||
var authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
|
var authorizeSuccessful = _tokens.ContainsKey(authInfo.ClientId) &&
|
||||||
|
_tokens[authInfo.ClientId].Token == authInfo.Token;
|
||||||
|
|
||||||
if (authorizeSuccessful)
|
if (authorizeSuccessful)
|
||||||
{
|
{
|
||||||
_tokens.TryRemove(networkId, out _);
|
_tokens.TryRemove(authInfo.ClientId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
return authorizeSuccessful;
|
return authorizeSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TokenState GenerateNextToken(long networkId)
|
public TokenState GenerateNextToken(ITokenIdentifier authInfo)
|
||||||
{
|
{
|
||||||
TokenState state;
|
TokenState state;
|
||||||
|
|
||||||
if (_tokens.ContainsKey(networkId))
|
if (_tokens.ContainsKey(authInfo.ClientId))
|
||||||
{
|
{
|
||||||
state = _tokens[networkId];
|
state = _tokens[authInfo.ClientId];
|
||||||
|
|
||||||
if ((DateTime.Now - state.RequestTime) > TimeoutPeriod)
|
if (DateTime.Now - state.RequestTime > TimeoutPeriod)
|
||||||
{
|
{
|
||||||
_tokens.TryRemove(networkId, out _);
|
_tokens.TryRemove(authInfo.ClientId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
@ -53,17 +54,16 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
|
|
||||||
state = new TokenState
|
state = new TokenState
|
||||||
{
|
{
|
||||||
NetworkId = networkId,
|
|
||||||
Token = _generateToken(),
|
Token = _generateToken(),
|
||||||
TokenDuration = TimeoutPeriod
|
TokenDuration = TimeoutPeriod
|
||||||
};
|
};
|
||||||
|
|
||||||
_tokens.TryAdd(networkId, state);
|
_tokens.TryAdd(authInfo.ClientId, state);
|
||||||
|
|
||||||
// perform some housekeeping so we don't have built up tokens if they're not ever used
|
// perform some housekeeping so we don't have built up tokens if they're not ever used
|
||||||
foreach (var (key, value) in _tokens)
|
foreach (var (key, value) in _tokens)
|
||||||
{
|
{
|
||||||
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod)
|
if (DateTime.Now - value.RequestTime > TimeoutPeriod)
|
||||||
{
|
{
|
||||||
_tokens.TryRemove(key, out _);
|
_tokens.TryRemove(key, out _);
|
||||||
}
|
}
|
||||||
|
207
Application/Plugin/PluginImporter.cs
Normal file
207
Application/Plugin/PluginImporter.cs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using IW4MAdmin.Application.API.Master;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementation of IPluginImporter
|
||||||
|
/// discovers plugins and script plugins
|
||||||
|
/// </summary>
|
||||||
|
public class PluginImporter : IPluginImporter
|
||||||
|
{
|
||||||
|
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
|
||||||
|
private const string PluginDir = "Plugins";
|
||||||
|
private const string PluginV2Match = "^ *((?:var|const|let) +init)|function init";
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
|
||||||
|
private readonly IMasterApi _masterApi;
|
||||||
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
|
||||||
|
private static readonly Type[] FilterTypes =
|
||||||
|
{
|
||||||
|
typeof(IPlugin),
|
||||||
|
typeof(IPluginV2),
|
||||||
|
typeof(Command),
|
||||||
|
typeof(IBaseConfiguration)
|
||||||
|
};
|
||||||
|
|
||||||
|
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi,
|
||||||
|
IRemoteAssemblyHandler remoteAssemblyHandler)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_masterApi = masterApi;
|
||||||
|
_remoteAssemblyHandler = remoteAssemblyHandler;
|
||||||
|
_appConfig = appConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// discovers all the script plugins in the plugins dir
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IEnumerable<(Type, string)> DiscoverScriptPlugins()
|
||||||
|
{
|
||||||
|
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
|
||||||
|
|
||||||
|
if (!Directory.Exists(pluginDir))
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<(Type, string)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var scriptPluginFiles =
|
||||||
|
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
|
||||||
|
|
||||||
|
var bothVersionPlugins = scriptPluginFiles.Select(fileName =>
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Discovered script plugin {FileName}", fileName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileContents = File.ReadAllLines(fileName);
|
||||||
|
var isValidV2 = fileContents.Any(line => Regex.IsMatch(line, PluginV2Match));
|
||||||
|
return isValidV2 ? (typeof(IPluginV2), fileName) : (typeof(IPlugin), fileName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (typeof(IPlugin), fileName);
|
||||||
|
}
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return bothVersionPlugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// discovers all the C# assembly plugins and commands
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
|
||||||
|
{
|
||||||
|
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
|
||||||
|
var pluginTypes = new List<Type>();
|
||||||
|
var commandTypes = new List<Type>();
|
||||||
|
var configurationTypes = new List<Type>();
|
||||||
|
|
||||||
|
if (!Directory.Exists(pluginDir))
|
||||||
|
{
|
||||||
|
return (pluginTypes, commandTypes, configurationTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
|
||||||
|
_logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length);
|
||||||
|
|
||||||
|
if (!dllFileNames.Any())
|
||||||
|
{
|
||||||
|
return (pluginTypes, commandTypes, configurationTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only want to load the most recent assembly in case of duplicates
|
||||||
|
var assemblies = dllFileNames.Select(Assembly.LoadFrom)
|
||||||
|
.Union(GetRemoteAssemblies())
|
||||||
|
.GroupBy(assembly => assembly.FullName).Select(assembly =>
|
||||||
|
assembly.OrderByDescending(asm => asm.GetName().Version).First());
|
||||||
|
|
||||||
|
var eligibleAssemblyTypes = assemblies
|
||||||
|
.SelectMany(asm =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return asm.GetTypes();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<Type>();
|
||||||
|
}
|
||||||
|
}).Where(type =>
|
||||||
|
FilterTypes.Any(filterType => type.GetInterface(filterType.Name, false) != null) ||
|
||||||
|
(type.IsClass && FilterTypes.Contains(type.BaseType)));
|
||||||
|
|
||||||
|
foreach (var assemblyType in eligibleAssemblyTypes)
|
||||||
|
{
|
||||||
|
var isPlugin =
|
||||||
|
(assemblyType.GetInterface(nameof(IPlugin), false) ??
|
||||||
|
assemblyType.GetInterface(nameof(IPluginV2), false)) != null &&
|
||||||
|
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
|
||||||
|
|
||||||
|
if (isPlugin)
|
||||||
|
{
|
||||||
|
pluginTypes.Add(assemblyType);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCommand = assemblyType.IsClass && assemblyType.BaseType == typeof(Command) &&
|
||||||
|
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
|
||||||
|
|
||||||
|
if (isCommand)
|
||||||
|
{
|
||||||
|
commandTypes.Add(assemblyType);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isConfiguration = assemblyType.IsClass &&
|
||||||
|
assemblyType.GetInterface(nameof(IBaseConfiguration), false) != null &&
|
||||||
|
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
|
||||||
|
|
||||||
|
if (isConfiguration)
|
||||||
|
{
|
||||||
|
configurationTypes.Add(assemblyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Discovered {Count} plugin implementations", pluginTypes.Count);
|
||||||
|
_logger.LogDebug("Discovered {Count} plugin command implementations", commandTypes.Count);
|
||||||
|
_logger.LogDebug("Discovered {Count} plugin configuration implementations", configurationTypes.Count);
|
||||||
|
|
||||||
|
return (pluginTypes, commandTypes, configurationTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Assembly> GetRemoteAssemblies()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pluginSubscription ??= _masterApi
|
||||||
|
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
|
||||||
|
|
||||||
|
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription
|
||||||
|
.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not load remote assemblies");
|
||||||
|
return Enumerable.Empty<Assembly>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetRemoteScripts()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pluginSubscription ??= _masterApi
|
||||||
|
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
|
||||||
|
|
||||||
|
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription
|
||||||
|
.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex,"Could not load remote scripts");
|
||||||
|
return Enumerable.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PluginType
|
||||||
|
{
|
||||||
|
Binary,
|
||||||
|
Script
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,17 @@
|
|||||||
using SharedLibraryCore;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models;
|
||||||
|
using Data.Models.Client;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Data.Models.Client;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
namespace IW4MAdmin.Application.Plugin.Script
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// generic script command implementation
|
/// generic script command implementation
|
||||||
@ -20,8 +23,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
|
|
||||||
public ScriptCommand(string name, string alias, string description, bool isTargetRequired,
|
public ScriptCommand(string name, string alias, string description, bool isTargetRequired,
|
||||||
EFClient.Permission permission,
|
EFClient.Permission permission,
|
||||||
CommandArgument[] args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
|
IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
|
||||||
ITranslationLookup layout, ILogger<ScriptCommand> logger, Server.Game[] supportedGames)
|
ITranslationLookup layout, ILogger<ScriptCommand> logger, IEnumerable<Reference.Game> supportedGames)
|
||||||
: base(config, layout)
|
: base(config, layout)
|
||||||
{
|
{
|
||||||
_executeAction = executeAction;
|
_executeAction = executeAction;
|
||||||
@ -31,8 +34,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
Description = description;
|
Description = description;
|
||||||
RequiresTarget = isTargetRequired;
|
RequiresTarget = isTargetRequired;
|
||||||
Permission = permission;
|
Permission = permission;
|
||||||
Arguments = args;
|
Arguments = args.ToArray();
|
||||||
SupportedGames = supportedGames;
|
SupportedGames = supportedGames?.Select(game => (Server.Game)game).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent e)
|
public override async Task ExecuteAsync(GameEvent e)
|
||||||
@ -48,7 +51,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
|
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {Command} {@Event}", Name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,24 +1,30 @@
|
|||||||
using System;
|
using System;
|
||||||
using Jint;
|
|
||||||
using Jint.Native;
|
|
||||||
using Jint.Runtime;
|
|
||||||
using Microsoft.CSharp.RuntimeBinder;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using SharedLibraryCore.Exceptions;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models;
|
||||||
|
using IW4MAdmin.Application.Configuration;
|
||||||
|
using IW4MAdmin.Application.Extensions;
|
||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
|
using Jint;
|
||||||
|
using Jint.Native;
|
||||||
|
using Jint.Runtime;
|
||||||
using Jint.Runtime.Interop;
|
using Jint.Runtime.Interop;
|
||||||
|
using Microsoft.CSharp.RuntimeBinder;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Commands;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
namespace IW4MAdmin.Application.Plugin.Script
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// implementation of IPlugin
|
/// implementation of IPlugin
|
||||||
@ -53,7 +59,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
Watcher = new FileSystemWatcher
|
Watcher = new FileSystemWatcher
|
||||||
{
|
{
|
||||||
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}",
|
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}",
|
||||||
NotifyFilter = NotifyFilters.Size,
|
NotifyFilter = NotifyFilters.LastWrite,
|
||||||
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
|
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,7 +74,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
|
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
|
||||||
IScriptPluginServiceResolver serviceResolver)
|
IScriptPluginServiceResolver serviceResolver, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -110,21 +116,33 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_scriptEngine?.Dispose();
|
||||||
_scriptEngine = new Engine(cfg =>
|
_scriptEngine = new Engine(cfg =>
|
||||||
cfg.AllowClr(new[]
|
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
|
||||||
|
typeof(ScriptPluginExtensions))
|
||||||
|
.AllowClr(new[]
|
||||||
{
|
{
|
||||||
typeof(System.Net.Http.HttpClient).Assembly,
|
typeof(System.Net.Http.HttpClient).Assembly,
|
||||||
typeof(EFClient).Assembly,
|
typeof(EFClient).Assembly,
|
||||||
typeof(Utilities).Assembly,
|
typeof(Utilities).Assembly,
|
||||||
typeof(Encoding).Assembly
|
typeof(Encoding).Assembly,
|
||||||
|
typeof(CancellationTokenSource).Assembly,
|
||||||
|
typeof(Data.Models.Client.EFClient).Assembly,
|
||||||
|
typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly
|
||||||
})
|
})
|
||||||
.CatchClrExceptions()
|
.CatchClrExceptions()
|
||||||
.AddObjectConverter(new PermissionLevelToStringConverter()));
|
.AddObjectConverter(new PermissionLevelToStringConverter()));
|
||||||
|
|
||||||
_scriptEngine.Execute(script);
|
_scriptEngine.Execute(script);
|
||||||
|
if (!_scriptEngine.GetValue("init").IsUndefined())
|
||||||
|
{
|
||||||
|
// this is a v2 plugin and we don't want to try to load it
|
||||||
|
Watcher.EnableRaisingEvents = false;
|
||||||
|
Watcher.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
|
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
|
||||||
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
|
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
|
||||||
_scriptEngine.SetValue("_lock", _onProcessing);
|
|
||||||
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
|
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
|
||||||
|
|
||||||
Author = pluginObject.author;
|
Author = pluginObject.author;
|
||||||
@ -160,11 +178,19 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task<bool> OnLoadTask()
|
||||||
|
{
|
||||||
|
await OnLoadAsync(manager);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadComplete = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (pluginObject.isParser)
|
if (pluginObject.isParser)
|
||||||
{
|
{
|
||||||
await OnLoadAsync(manager);
|
loadComplete = await OnLoadTask();
|
||||||
IsParser = true;
|
IsParser = true;
|
||||||
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject();
|
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject();
|
||||||
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject();
|
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject();
|
||||||
@ -175,32 +201,35 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
|
|
||||||
catch (RuntimeBinderException)
|
catch (RuntimeBinderException)
|
||||||
{
|
{
|
||||||
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
|
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine, configHandler);
|
||||||
await configWrapper.InitializeAsync();
|
|
||||||
_scriptEngine.SetValue("_configHandler", configWrapper);
|
|
||||||
await OnLoadAsync(manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!firstRun)
|
if (!loadComplete)
|
||||||
{
|
{
|
||||||
await OnLoadAsync(manager);
|
_scriptEngine.SetValue("_configHandler", configWrapper);
|
||||||
|
loadComplete = await OnLoadTask();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_successfullyLoaded = true;
|
if (!firstRun && !loadComplete)
|
||||||
|
{
|
||||||
|
loadComplete = await OnLoadTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
_successfullyLoaded = loadComplete;
|
||||||
}
|
}
|
||||||
catch (JavaScriptException ex)
|
catch (JavaScriptException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
_logger.LogError(ex,
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace}",
|
||||||
nameof(Initialize), Path.GetFileName(_fileName), ex.Location);
|
nameof(Initialize), Path.GetFileName(_fileName), ex.Location, ex.JavaScriptStackTrace);
|
||||||
|
|
||||||
throw new PluginException("An error occured while initializing script plugin");
|
throw new PluginException("An error occured while initializing script plugin");
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
|
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
_logger.LogError(ex,
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo}",
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace}",
|
||||||
nameof(Initialize), _fileName, jsEx.Location);
|
nameof(Initialize), _fileName, jsEx.Location, jsEx.JavaScriptStackTrace);
|
||||||
|
|
||||||
throw new PluginException("An error occured while initializing script plugin");
|
throw new PluginException("An error occured while initializing script plugin");
|
||||||
}
|
}
|
||||||
@ -228,39 +257,62 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldRelease = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _onProcessing.WaitAsync();
|
await _onProcessing.WaitAsync(Utilities.DefaultCommandTimeout / 2);
|
||||||
|
shouldRelease = true;
|
||||||
|
WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
_scriptEngine.SetValue("_gameEvent", gameEvent);
|
_scriptEngine.SetValue("_gameEvent", gameEvent);
|
||||||
_scriptEngine.SetValue("_server", server);
|
_scriptEngine.SetValue("_server", server);
|
||||||
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
|
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
|
||||||
_scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
|
return _scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
|
||||||
|
}, new { EventType = gameEvent.Type }, server);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
catch (JavaScriptException ex)
|
|
||||||
{
|
{
|
||||||
using (LogContext.PushProperty("Server", server.ToString()))
|
if (_onProcessing.CurrentCount == 0 && shouldRelease)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
_onProcessing.Release(1);
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} with event type {EventType} {@LocationInfo}",
|
}
|
||||||
nameof(OnEventAsync), Path.GetFileName(_fileName), gameEvent.Type, ex.Location);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new PluginException("An error occured while executing action for script plugin");
|
public Task OnLoadAsync(IManager manager)
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
using (LogContext.PushProperty("Server", server.ToString()))
|
_logger.LogDebug("OnLoad executing for {Name}", Name);
|
||||||
|
|
||||||
|
WrapJavaScriptErrorHandling(() =>
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
_scriptEngine.SetValue("_manager", manager);
|
||||||
"Encountered error while running {MethodName} for script plugin {Plugin} with event type {EventType}",
|
return _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
|
||||||
nameof(OnEventAsync), _fileName, gameEvent.Type);
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new PluginException("An error occured while executing action for script plugin");
|
public Task OnTickAsync(Server server)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task OnUnloadAsync()
|
||||||
|
{
|
||||||
|
if (!_successfullyLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _onProcessing.WaitAsync();
|
||||||
|
|
||||||
|
_logger.LogDebug("OnUnload executing for {Name}", Name);
|
||||||
|
|
||||||
|
WrapJavaScriptErrorHandling(() => _scriptEngine.Evaluate("plugin.onUnloadAsync()"));
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (_onProcessing.CurrentCount == 0)
|
if (_onProcessing.CurrentCount == 0)
|
||||||
@ -270,71 +322,72 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnLoadAsync(IManager manager)
|
public T ExecuteAction<T>(Delegate action, CancellationToken token, params object[] param)
|
||||||
{
|
{
|
||||||
try
|
var shouldRelease = false;
|
||||||
{
|
|
||||||
_logger.LogDebug("OnLoad executing for {Name}", Name);
|
|
||||||
_scriptEngine.SetValue("_manager", manager);
|
|
||||||
_scriptEngine.SetValue("getDvar", GetDvarAsync);
|
|
||||||
_scriptEngine.SetValue("setDvar", SetDvarAsync);
|
|
||||||
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
catch (JavaScriptException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex,
|
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
|
|
||||||
nameof(OnLoadAsync), Path.GetFileName(_fileName), ex.Location);
|
|
||||||
|
|
||||||
throw new PluginException("A runtime error occured while executing action for script plugin");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex,
|
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
|
|
||||||
nameof(OnLoadAsync), Path.GetFileName(_fileName));
|
|
||||||
|
|
||||||
throw new PluginException("An error occured while executing action for script plugin");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnTickAsync(Server server)
|
|
||||||
{
|
|
||||||
_scriptEngine.SetValue("_server", server);
|
|
||||||
await Task.FromResult(_scriptEngine.Evaluate("plugin.onTickAsync(_server)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnUnloadAsync()
|
|
||||||
{
|
|
||||||
if (!_successfullyLoaded)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_scriptEngine.Evaluate("plugin.onUnloadAsync()");
|
using var forceTimeout = new CancellationTokenSource(5000);
|
||||||
}
|
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
|
||||||
catch (JavaScriptException ex)
|
_onProcessing.Wait(combined.Token);
|
||||||
|
shouldRelease = true;
|
||||||
|
|
||||||
|
_logger.LogDebug("Executing action for {Name}", Name);
|
||||||
|
|
||||||
|
return WrapJavaScriptErrorHandling(T() =>
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
|
var result = action.DynamicInvoke(JsValue.Undefined, args);
|
||||||
nameof(OnUnloadAsync), Path.GetFileName(_fileName), ex.Location);
|
return (T)(result as JsValue)?.ToObject();
|
||||||
|
},
|
||||||
throw new PluginException("A runtime error occured while executing action for script plugin");
|
new
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
Params = string.Join(", ",
|
||||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
|
param?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
|
||||||
nameof(OnUnloadAsync), Path.GetFileName(_fileName));
|
Enumerable.Empty<string>())
|
||||||
|
});
|
||||||
throw new PluginException("An error occured while executing action for script plugin");
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onProcessing.CurrentCount == 0 && shouldRelease)
|
||||||
|
{
|
||||||
|
_onProcessing.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
public T WrapDelegate<T>(Delegate act, CancellationToken token, params object[] args)
|
||||||
|
{
|
||||||
|
var shouldRelease = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var forceTimeout = new CancellationTokenSource(5000);
|
||||||
|
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
|
||||||
|
_onProcessing.Wait(combined.Token);
|
||||||
|
shouldRelease = true;
|
||||||
|
|
||||||
|
_logger.LogDebug("Wrapping delegate action for {Name}", Name);
|
||||||
|
|
||||||
|
return WrapJavaScriptErrorHandling(
|
||||||
|
T() => (T)(act.DynamicInvoke(JsValue.Null,
|
||||||
|
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)
|
||||||
|
?.ToObject(),
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Params = string.Join(", ",
|
||||||
|
args?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
|
||||||
|
Enumerable.Empty<string>())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_onProcessing.CurrentCount == 0 && shouldRelease)
|
||||||
|
{
|
||||||
|
_onProcessing.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -343,7 +396,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
/// <param name="commands">commands value from jint parser</param>
|
/// <param name="commands">commands value from jint parser</param>
|
||||||
/// <param name="scriptCommandFactory">factory to create the command from</param>
|
/// <param name="scriptCommandFactory">factory to create the command from</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
|
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
|
||||||
|
IScriptCommandFactory scriptCommandFactory)
|
||||||
{
|
{
|
||||||
var commandList = new List<IManagerCommand>();
|
var commandList = new List<IManagerCommand>();
|
||||||
|
|
||||||
@ -354,11 +408,17 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
string name = dynamicCommand.name;
|
string name = dynamicCommand.name;
|
||||||
string alias = dynamicCommand.alias;
|
string alias = dynamicCommand.alias;
|
||||||
string description = dynamicCommand.description;
|
string description = dynamicCommand.description;
|
||||||
|
|
||||||
|
if (dynamicCommand.permission is Data.Models.Client.EFClient.Permission perm)
|
||||||
|
{
|
||||||
|
dynamicCommand.permission = perm.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
string permission = dynamicCommand.permission;
|
string permission = dynamicCommand.permission;
|
||||||
List<Server.Game> supportedGames = null;
|
List<Reference.Game> supportedGames = null;
|
||||||
var targetRequired = false;
|
var targetRequired = false;
|
||||||
|
|
||||||
var args = new List<(string, bool)>();
|
var args = new List<CommandArgument>();
|
||||||
dynamic arguments = null;
|
dynamic arguments = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -385,7 +445,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
foreach (var arg in dynamicCommand.arguments)
|
foreach (var arg in dynamicCommand.arguments)
|
||||||
{
|
{
|
||||||
args.Add((arg.name, (bool)arg.required));
|
args.Add(new CommandArgument { Name = arg.name, Required = (bool)arg.required });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,8 +453,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
foreach (var game in dynamicCommand.supportedGames)
|
foreach (var game in dynamicCommand.supportedGames)
|
||||||
{
|
{
|
||||||
supportedGames ??= new List<Server.Game>();
|
supportedGames ??= new List<Reference.Game>();
|
||||||
supportedGames.Add(Enum.Parse(typeof(Server.Game), game.ToString()));
|
supportedGames.Add(Enum.Parse(typeof(Reference.Game), game.ToString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (RuntimeBinderException)
|
catch (RuntimeBinderException)
|
||||||
@ -437,7 +497,6 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
throw new PluginException("An error occured while executing action for script plugin");
|
throw new PluginException("An error occured while executing action for script plugin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (_onProcessing.CurrentCount == 0)
|
if (_onProcessing.CurrentCount == 0)
|
||||||
@ -448,89 +507,46 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
}
|
}
|
||||||
|
|
||||||
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission,
|
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission,
|
||||||
targetRequired, args, Execute, supportedGames?.ToArray()));
|
targetRequired, args, Execute, supportedGames));
|
||||||
}
|
}
|
||||||
|
|
||||||
return commandList;
|
return commandList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted)
|
private T WrapJavaScriptErrorHandling<T>(Func<T> work, object additionalData = null, Server server = null,
|
||||||
|
[CallerMemberName] string methodName = "")
|
||||||
{
|
{
|
||||||
Task.Run<Task>(async () =>
|
using (LogContext.PushProperty("Server", server?.ToString()))
|
||||||
{
|
{
|
||||||
var tokenSource = new CancellationTokenSource();
|
|
||||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
|
||||||
string result = null;
|
|
||||||
var success = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = (await server.GetDvarAsync<string>(dvarName, token: tokenSource.Token)).Value;
|
return work();
|
||||||
}
|
}
|
||||||
catch
|
catch (JavaScriptException ex)
|
||||||
{
|
{
|
||||||
success = false;
|
_logger.LogError(ex,
|
||||||
}
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
|
||||||
|
methodName, Path.GetFileName(_fileName), ex.Location, ex.StackTrace, additionalData);
|
||||||
|
|
||||||
await _onProcessing.WaitAsync();
|
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||||
try
|
|
||||||
{
|
|
||||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
JsValue.FromObject(_scriptEngine, server),
|
|
||||||
JsValue.FromObject(_scriptEngine, dvarName),
|
|
||||||
JsValue.FromObject(_scriptEngine, result),
|
|
||||||
JsValue.FromObject(_scriptEngine, success),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
|
||||||
|
methodName, _fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
|
||||||
|
|
||||||
finally
|
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||||
{
|
|
||||||
if (_onProcessing.CurrentCount == 0)
|
|
||||||
{
|
|
||||||
_onProcessing.Release();
|
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
});
|
|
||||||
}
|
|
||||||
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
|
|
||||||
{
|
{
|
||||||
Task.Run<Task>(async () =>
|
_logger.LogError(ex,
|
||||||
{
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
|
||||||
var tokenSource = new CancellationTokenSource();
|
methodName, Path.GetFileName(_fileName));
|
||||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
|
||||||
var success = true;
|
|
||||||
|
|
||||||
try
|
throw new PluginException("An error occured while executing action for script plugin");
|
||||||
{
|
|
||||||
await server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _onProcessing.WaitAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
JsValue.FromObject(_scriptEngine, server),
|
|
||||||
JsValue.FromObject(_scriptEngine, dvarName),
|
|
||||||
JsValue.FromObject(_scriptEngine, dvarValue),
|
|
||||||
JsValue.FromObject(_scriptEngine, success)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (_onProcessing.CurrentCount == 0)
|
|
||||||
{
|
|
||||||
_onProcessing.Release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,7 +560,6 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
result = JsValue.Null;
|
result = JsValue.Null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
130
Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs
Normal file
130
Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using IW4MAdmin.Application.Configuration;
|
||||||
|
using Jint;
|
||||||
|
using Jint.Native;
|
||||||
|
using Jint.Native.Json;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Plugin.Script;
|
||||||
|
|
||||||
|
public class ScriptPluginConfigurationWrapper
|
||||||
|
{
|
||||||
|
public event Action<JsValue, Delegate> ConfigurationUpdated;
|
||||||
|
|
||||||
|
private readonly ScriptPluginConfiguration _config;
|
||||||
|
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
|
||||||
|
private readonly Engine _scriptEngine;
|
||||||
|
private readonly JsonParser _engineParser;
|
||||||
|
private readonly List<(string, Delegate)> _updateCallbackActions = new();
|
||||||
|
private string _pluginName;
|
||||||
|
|
||||||
|
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
|
||||||
|
{
|
||||||
|
_pluginName = pluginName;
|
||||||
|
_scriptEngine = scriptEngine;
|
||||||
|
_configHandler = configHandler;
|
||||||
|
_configHandler.Updated += OnConfigurationUpdated;
|
||||||
|
_config = configHandler.Get("ScriptPluginSettings", new ScriptPluginConfiguration()).GetAwaiter().GetResult();
|
||||||
|
_engineParser = new JsonParser(_scriptEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScriptPluginConfigurationWrapper()
|
||||||
|
{
|
||||||
|
_configHandler.Updated -= OnConfigurationUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetName(string name)
|
||||||
|
{
|
||||||
|
_pluginName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetValue(string key, object value)
|
||||||
|
{
|
||||||
|
var castValue = value;
|
||||||
|
|
||||||
|
if (value is double doubleValue)
|
||||||
|
{
|
||||||
|
castValue = AsInteger(doubleValue) ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
|
||||||
|
{
|
||||||
|
castValue = array.Select(item => AsInteger((double)item)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_config.ContainsKey(_pluginName))
|
||||||
|
{
|
||||||
|
_config.Add(_pluginName, new Dictionary<string, object>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugin = _config[_pluginName];
|
||||||
|
|
||||||
|
if (plugin.ContainsKey(key))
|
||||||
|
{
|
||||||
|
plugin[key] = castValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
plugin.Add(key, castValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _configHandler.Set(_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsValue GetValue(string key) => GetValue(key, null);
|
||||||
|
|
||||||
|
public JsValue GetValue(string key, Delegate updateCallback)
|
||||||
|
{
|
||||||
|
if (!_config.ContainsKey(_pluginName))
|
||||||
|
{
|
||||||
|
return JsValue.Undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_config[_pluginName].ContainsKey(key))
|
||||||
|
{
|
||||||
|
return JsValue.Undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = _config[_pluginName][key];
|
||||||
|
|
||||||
|
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
|
||||||
|
{
|
||||||
|
item = jElem.Deserialize<List<dynamic>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateCallback is not null)
|
||||||
|
{
|
||||||
|
_updateCallbackActions.Add((key, updateCallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _engineParser.Parse(item!.ToString()!);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsValue.FromObject(_scriptEngine, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? AsInteger(double value)
|
||||||
|
{
|
||||||
|
return int.TryParse(value.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConfigurationUpdated(ScriptPluginConfiguration config)
|
||||||
|
{
|
||||||
|
foreach (var callback in _updateCallbackActions)
|
||||||
|
{
|
||||||
|
ConfigurationUpdated?.Invoke(GetValue(callback.Item1), callback.Item2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Application/Plugin/Script/ScriptPluginFactory.cs
Normal file
32
Application/Plugin/Script/ScriptPluginFactory.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using IW4MAdmin.Application.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Plugin.Script;
|
||||||
|
|
||||||
|
public class ScriptPluginFactory : IScriptPluginFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public ScriptPluginFactory(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object CreateScriptPlugin(Type type, string fileName)
|
||||||
|
{
|
||||||
|
if (type == typeof(IPlugin))
|
||||||
|
{
|
||||||
|
return new ScriptPlugin(_serviceProvider.GetRequiredService<ILogger<ScriptPlugin>>(),
|
||||||
|
fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ScriptPluginV2(fileName, _serviceProvider.GetRequiredService<ILogger<ScriptPluginV2>>(),
|
||||||
|
_serviceProvider.GetRequiredService<IScriptPluginServiceResolver>(),
|
||||||
|
_serviceProvider.GetRequiredService<IScriptCommandFactory>(),
|
||||||
|
_serviceProvider.GetRequiredService<IConfigurationHandlerV2<ScriptPluginConfiguration>>(),
|
||||||
|
_serviceProvider.GetRequiredService<IInteractionRegistration>());
|
||||||
|
}
|
||||||
|
}
|
143
Application/Plugin/Script/ScriptPluginHelper.cs
Normal file
143
Application/Plugin/Script/ScriptPluginHelper.cs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jint.Native;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Plugin.Script;
|
||||||
|
|
||||||
|
public class ScriptPluginHelper
|
||||||
|
{
|
||||||
|
private readonly IManager _manager;
|
||||||
|
private readonly ScriptPluginV2 _scriptPlugin;
|
||||||
|
private readonly SemaphoreSlim _onRequestRunning = new(1, 1);
|
||||||
|
private const int RequestTimeout = 5000;
|
||||||
|
|
||||||
|
public ScriptPluginHelper(IManager manager, ScriptPluginV2 scriptPlugin)
|
||||||
|
{
|
||||||
|
_manager = manager;
|
||||||
|
_scriptPlugin = scriptPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetUrl(string url, Delegate callback)
|
||||||
|
{
|
||||||
|
RequestUrl(new ScriptPluginWebRequest(url), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetUrl(string url, string bearerToken, Delegate callback)
|
||||||
|
{
|
||||||
|
var headers = new Dictionary<string, string> { { "Authorization", $"Bearer {bearerToken}" } };
|
||||||
|
RequestUrl(new ScriptPluginWebRequest(url, Headers: headers), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PostUrl(string url, string body, string bearerToken, Delegate callback)
|
||||||
|
{
|
||||||
|
var headers = new Dictionary<string, string> { { "Authorization", $"Bearer {bearerToken}" } };
|
||||||
|
RequestUrl(
|
||||||
|
new ScriptPluginWebRequest(url, body, "POST", Headers: headers), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestUrl(ScriptPluginWebRequest request, Delegate callback)
|
||||||
|
{
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = RequestInternal(request);
|
||||||
|
_scriptPlugin.ExecuteWithErrorHandling(scriptEngine =>
|
||||||
|
{
|
||||||
|
callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.FromObject(scriptEngine, response) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestNotifyAfterDelay(int delayMs, Delegate callback)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(delayMs, _manager.CancellationToken);
|
||||||
|
_scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterDynamicCommand(JsValue command)
|
||||||
|
{
|
||||||
|
_scriptPlugin.RegisterDynamicCommand(command.ToObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
private object RequestInternal(ScriptPluginWebRequest request)
|
||||||
|
{
|
||||||
|
var entered = false;
|
||||||
|
using var tokenSource = new CancellationTokenSource(RequestTimeout);
|
||||||
|
using var client = new HttpClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_onRequestRunning.Wait(tokenSource.Token);
|
||||||
|
|
||||||
|
entered = true;
|
||||||
|
var requestMessage = new HttpRequestMessage(new HttpMethod(request.Method), request.Url);
|
||||||
|
|
||||||
|
if (request.Body is not null)
|
||||||
|
{
|
||||||
|
requestMessage.Content = new StringContent(request.Body.ToString() ?? string.Empty, Encoding.Default,
|
||||||
|
request.ContentType ?? "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Headers is not null)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in request.Headers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
requestMessage.Headers.Add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = client.Send(requestMessage, tokenSource.Token);
|
||||||
|
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
ex.StatusCode,
|
||||||
|
ex.Message,
|
||||||
|
IsError = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
ex.Message,
|
||||||
|
IsError = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (entered)
|
||||||
|
{
|
||||||
|
_onRequestRunning.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
using SharedLibraryCore.Interfaces;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc
|
namespace IW4MAdmin.Application.Plugin.Script
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// implementation of IScriptPluginServiceResolver
|
/// implementation of IScriptPluginServiceResolver
|
||||||
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
public object ResolveService(string serviceName, string[] genericParameters)
|
public object ResolveService(string serviceName, string[] genericParameters)
|
||||||
{
|
{
|
||||||
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
|
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
|
||||||
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
|
var genericTypes = genericParameters.Select(genericTypeParam => DetermineRootType(genericTypeParam));
|
||||||
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
|
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
|
||||||
return _serviceProvider.GetService(resolvedServiceType);
|
return _serviceProvider.GetService(resolvedServiceType);
|
||||||
}
|
}
|
||||||
@ -34,8 +34,8 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
{
|
{
|
||||||
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
|
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
.SelectMany(t => t.GetTypes());
|
.SelectMany(t => t.GetTypes());
|
||||||
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
var generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
||||||
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
|
var serviceType = typeCollection.FirstOrDefault(type => type.Name.ToLower() == generatedName);
|
||||||
|
|
||||||
if (serviceType == null)
|
if (serviceType == null)
|
||||||
{
|
{
|
@ -6,18 +6,22 @@ using Microsoft.Extensions.Logging;
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Misc;
|
namespace IW4MAdmin.Application.Plugin.Script;
|
||||||
|
|
||||||
|
[Obsolete("This architecture is superseded by the request notify delay architecture")]
|
||||||
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
||||||
{
|
{
|
||||||
private Timer _timer;
|
private Timer _timer;
|
||||||
private Action _actions;
|
private Action _actions;
|
||||||
private Delegate _jsAction;
|
private Delegate _jsAction;
|
||||||
private string _actionName;
|
private string _actionName;
|
||||||
|
private int _interval = DefaultInterval;
|
||||||
|
private long _waitingCount;
|
||||||
private const int DefaultDelay = 0;
|
private const int DefaultDelay = 0;
|
||||||
private const int DefaultInterval = 1000;
|
private const int DefaultInterval = 1000;
|
||||||
|
private const int MaxWaiting = 10;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ManualResetEventSlim _onRunningTick = new();
|
private readonly SemaphoreSlim _onRunningTick = new(1, 1);
|
||||||
private SemaphoreSlim _onDependentAction;
|
private SemaphoreSlim _onDependentAction;
|
||||||
|
|
||||||
public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger)
|
public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger)
|
||||||
@ -31,6 +35,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
|||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRunningTick.Dispose();
|
_onRunningTick.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +60,8 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
|||||||
|
|
||||||
_logger.LogDebug("Starting script timer...");
|
_logger.LogDebug("Starting script timer...");
|
||||||
|
|
||||||
_onRunningTick.Set();
|
|
||||||
_timer ??= new Timer(callback => _actions(), null, delay, interval);
|
_timer ??= new Timer(callback => _actions(), null, delay, interval);
|
||||||
|
_interval = interval;
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,53 +105,90 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
|||||||
|
|
||||||
_jsAction = action;
|
_jsAction = action;
|
||||||
_actionName = actionName;
|
_actionName = actionName;
|
||||||
_actions = OnTick;
|
_actions = OnTickInternal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReleaseThreads()
|
private void ReleaseThreads(bool releaseOnRunning, bool releaseOnDependent)
|
||||||
{
|
{
|
||||||
_onRunningTick.Set();
|
if (releaseOnRunning && _onRunningTick.CurrentCount == 0)
|
||||||
|
|
||||||
if (_onDependentAction?.CurrentCount != 0)
|
|
||||||
{
|
{
|
||||||
return;
|
_logger.LogDebug("-Releasing OnRunning for timer");
|
||||||
|
_onRunningTick.Release(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (releaseOnDependent && _onDependentAction?.CurrentCount == 0)
|
||||||
|
{
|
||||||
_onDependentAction?.Release(1);
|
_onDependentAction?.Release(1);
|
||||||
}
|
}
|
||||||
private void OnTick()
|
}
|
||||||
|
|
||||||
|
private async void OnTickInternal()
|
||||||
|
{
|
||||||
|
var releaseOnRunning = false;
|
||||||
|
var releaseOnDependent = false;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_onRunningTick.IsSet)
|
if (Interlocked.Read(ref _waitingCount) > MaxWaiting)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one",
|
_logger.LogWarning("Reached max number of waiting count ({WaitingCount}) for {OnTick}",
|
||||||
nameof(OnTick));
|
_waitingCount, nameof(OnTickInternal));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRunningTick.Reset();
|
Interlocked.Increment(ref _waitingCount);
|
||||||
|
using var tokenSource1 = new CancellationTokenSource();
|
||||||
|
tokenSource1.CancelAfter(TimeSpan.FromMilliseconds(_interval));
|
||||||
|
await _onRunningTick.WaitAsync(tokenSource1.Token);
|
||||||
|
releaseOnRunning = true;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one",
|
||||||
|
nameof(OnTickInternal));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var tokenSource = new CancellationTokenSource();
|
||||||
|
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
|
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
|
||||||
_onDependentAction?.WaitAsync().Wait();
|
if (_onDependentAction is not null)
|
||||||
|
{
|
||||||
|
await _onDependentAction.WaitAsync(tokenSource.Token);
|
||||||
|
releaseOnDependent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Dependent action did not release in allotted time so we are cancelling this tick");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("+Running OnTick for timer");
|
||||||
var start = DateTime.Now;
|
var start = DateTime.Now;
|
||||||
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
|
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
|
||||||
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
|
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
|
||||||
ReleaseThreads();
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (ex.InnerException is JavaScriptException jsx)
|
||||||
catch (Exception ex) when (ex.InnerException is JavaScriptException jsex)
|
|
||||||
{
|
{
|
||||||
_logger.LogError(jsex,
|
_logger.LogError(jsx,
|
||||||
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName,
|
"Could not execute timer tick for script action {ActionName} [{@LocationInfo}] [{@StackTrace}]",
|
||||||
jsex.Location);
|
_actionName,
|
||||||
ReleaseThreads();
|
jsx.Location, jsx.JavaScriptStackTrace);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
|
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
|
||||||
_onRunningTick.Set();
|
}
|
||||||
ReleaseThreads();
|
finally
|
||||||
|
{
|
||||||
|
ReleaseThreads(releaseOnRunning, releaseOnDependent);
|
||||||
|
Interlocked.Decrement(ref _waitingCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
597
Application/Plugin/Script/ScriptPluginV2.cs
Normal file
597
Application/Plugin/Script/ScriptPluginV2.cs
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Dynamic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models;
|
||||||
|
using IW4MAdmin.Application.Configuration;
|
||||||
|
using IW4MAdmin.Application.Extensions;
|
||||||
|
using Jint;
|
||||||
|
using Jint.Native;
|
||||||
|
using Jint.Runtime;
|
||||||
|
using Jint.Runtime.Interop;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog.Context;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Commands;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Events.Server;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.Interfaces.Events;
|
||||||
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
using JavascriptEngine = Jint.Engine;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Plugin.Script;
|
||||||
|
|
||||||
|
public class ScriptPluginV2 : IPluginV2
|
||||||
|
{
|
||||||
|
public string Name { get; private set; } = string.Empty;
|
||||||
|
public string Author { get; private set; } = string.Empty;
|
||||||
|
public string Version { get; private set; }
|
||||||
|
|
||||||
|
private readonly string _fileName;
|
||||||
|
private readonly ILogger<ScriptPluginV2> _logger;
|
||||||
|
private readonly IScriptPluginServiceResolver _pluginServiceResolver;
|
||||||
|
private readonly IScriptCommandFactory _scriptCommandFactory;
|
||||||
|
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
|
||||||
|
private readonly IInteractionRegistration _interactionRegistration;
|
||||||
|
private readonly SemaphoreSlim _onProcessingScript = new(1, 1);
|
||||||
|
private readonly SemaphoreSlim _onLoadingFile = new(1, 1);
|
||||||
|
private readonly FileSystemWatcher _scriptWatcher;
|
||||||
|
private readonly List<string> _registeredCommandNames = new();
|
||||||
|
private readonly List<string> _registeredInteractions = new();
|
||||||
|
private readonly Dictionary<MethodInfo, List<object>> _registeredEvents = new();
|
||||||
|
private IManager _manager;
|
||||||
|
private bool _firstInitialization = true;
|
||||||
|
|
||||||
|
private record ScriptPluginDetails(string Name, string Author, string Version,
|
||||||
|
ScriptPluginCommandDetails[] Commands, ScriptPluginInteractionDetails[] Interactions);
|
||||||
|
|
||||||
|
private record ScriptPluginCommandDetails(string Name, string Description, string Alias, string Permission,
|
||||||
|
bool TargetRequired, CommandArgument[] Arguments, IEnumerable<Reference.Game> SupportedGames, Delegate Execute);
|
||||||
|
|
||||||
|
private JavascriptEngine ScriptEngine
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (ActiveEngines)
|
||||||
|
{
|
||||||
|
return ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ScriptPluginInteractionDetails(string Name, Delegate Action);
|
||||||
|
|
||||||
|
private ScriptPluginConfigurationWrapper _scriptPluginConfigurationWrapper;
|
||||||
|
private int _nextEngineId;
|
||||||
|
private static readonly Dictionary<string, JavascriptEngine> ActiveEngines = new();
|
||||||
|
|
||||||
|
public ScriptPluginV2(string fileName, ILogger<ScriptPluginV2> logger,
|
||||||
|
IScriptPluginServiceResolver pluginServiceResolver, IScriptCommandFactory scriptCommandFactory,
|
||||||
|
IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler,
|
||||||
|
IInteractionRegistration interactionRegistration)
|
||||||
|
{
|
||||||
|
_fileName = fileName;
|
||||||
|
_logger = logger;
|
||||||
|
_pluginServiceResolver = pluginServiceResolver;
|
||||||
|
_scriptCommandFactory = scriptCommandFactory;
|
||||||
|
_configHandler = configHandler;
|
||||||
|
_interactionRegistration = interactionRegistration;
|
||||||
|
_scriptWatcher = new FileSystemWatcher
|
||||||
|
{
|
||||||
|
Path = Path.Join(Utilities.OperatingDirectory, "Plugins"),
|
||||||
|
NotifyFilter = NotifyFilters.LastWrite,
|
||||||
|
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
|
||||||
|
};
|
||||||
|
|
||||||
|
IManagementEventSubscriptions.Load += OnLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteWithErrorHandling(Action<Engine> work)
|
||||||
|
{
|
||||||
|
WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
work(ScriptEngine);
|
||||||
|
return true;
|
||||||
|
}, _logger, _fileName, _onProcessingScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object QueryWithErrorHandling(Delegate action, params object[] args)
|
||||||
|
{
|
||||||
|
return WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
var jsArgs = args?.Select(param => JsValue.FromObject(ScriptEngine, param)).ToArray();
|
||||||
|
var result = action.DynamicInvoke(JsValue.Undefined, jsArgs);
|
||||||
|
return result;
|
||||||
|
}, _logger, _fileName, _onProcessingScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterDynamicCommand(object command)
|
||||||
|
{
|
||||||
|
var parsedCommand = ParseScriptCommandDetails(command);
|
||||||
|
RegisterCommand(_manager, parsedCommand.First());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnLoad(IManager manager, CancellationToken token)
|
||||||
|
{
|
||||||
|
_manager = manager;
|
||||||
|
var entered = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _onLoadingFile.WaitAsync(token);
|
||||||
|
entered = true;
|
||||||
|
|
||||||
|
_logger.LogDebug("{Method} executing for {Plugin}", nameof(OnLoad), _fileName);
|
||||||
|
|
||||||
|
if (new FileInfo(_fileName).Length == 0L)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scriptWatcher.EnableRaisingEvents = false;
|
||||||
|
|
||||||
|
UnregisterScriptEntities(manager);
|
||||||
|
ResetEngineState();
|
||||||
|
|
||||||
|
if (_firstInitialization)
|
||||||
|
{
|
||||||
|
_scriptWatcher.Changed += async (_, _) => await OnLoad(manager, token);
|
||||||
|
_firstInitialization = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var stream =
|
||||||
|
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
using var reader = new StreamReader(stream, Encoding.Default);
|
||||||
|
var pluginScript = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
var pluginDetails = WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptEngine.Execute(pluginScript);
|
||||||
|
#pragma warning disable CS8974
|
||||||
|
var initResult = ScriptEngine.Call("init", JsValue.FromObject(ScriptEngine, EventCallbackWrapper),
|
||||||
|
JsValue.FromObject(ScriptEngine, _pluginServiceResolver),
|
||||||
|
JsValue.FromObject(ScriptEngine, _scriptPluginConfigurationWrapper),
|
||||||
|
JsValue.FromObject(ScriptEngine, new ScriptPluginHelper(manager, this)));
|
||||||
|
#pragma warning restore CS8974
|
||||||
|
|
||||||
|
if (initResult.IsNull() || initResult.IsUndefined())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsScriptPluginInstance(initResult.ToObject());
|
||||||
|
}, _logger, _fileName, _onProcessingScript);
|
||||||
|
|
||||||
|
if (pluginDetails is null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No valid script plugin signature found for {FilePath}", _fileName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var command in pluginDetails.Commands)
|
||||||
|
{
|
||||||
|
RegisterCommand(manager, command);
|
||||||
|
|
||||||
|
_logger.LogDebug("Registered script plugin command {Command} for {Plugin}", command.Name,
|
||||||
|
pluginDetails.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var interaction in pluginDetails.Interactions)
|
||||||
|
{
|
||||||
|
RegisterInteraction(interaction);
|
||||||
|
|
||||||
|
_logger.LogDebug("Registered script plugin interaction {Interaction} for {Plugin}", interaction.Name,
|
||||||
|
pluginDetails.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Name = pluginDetails.Name;
|
||||||
|
Author = pluginDetails.Author;
|
||||||
|
Version = pluginDetails.Version;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unexpected error encountered loading script plugin {Name}", _fileName);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (entered)
|
||||||
|
{
|
||||||
|
_onLoadingFile.Release(1);
|
||||||
|
_scriptWatcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("{Method} completed for {Plugin}", nameof(OnLoad), _fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterInteraction(ScriptPluginInteractionDetails interaction)
|
||||||
|
{
|
||||||
|
Task<IInteractionData> Action(int? targetId, Reference.Game? game, CancellationToken token) =>
|
||||||
|
WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = new object[] { targetId, game, token }.Select(arg => JsValue.FromObject(ScriptEngine, arg))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (interaction.Action.DynamicInvoke(JsValue.Undefined, args) is not ObjectWrapper result)
|
||||||
|
{
|
||||||
|
throw new PluginException("Invalid interaction object returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult((IInteractionData)result.ToObject());
|
||||||
|
}, _logger, _fileName, _onProcessingScript);
|
||||||
|
|
||||||
|
_interactionRegistration.RegisterInteraction(interaction.Name, Action);
|
||||||
|
_registeredInteractions.Add(interaction.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterCommand(IManager manager, ScriptPluginCommandDetails command)
|
||||||
|
{
|
||||||
|
Task Execute(GameEvent gameEvent) =>
|
||||||
|
WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
command.Execute.DynamicInvoke(JsValue.Undefined,
|
||||||
|
new[] { JsValue.FromObject(ScriptEngine, gameEvent) });
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, _logger, _fileName, _onProcessingScript);
|
||||||
|
|
||||||
|
var scriptCommand = _scriptCommandFactory.CreateScriptCommand(command.Name, command.Alias,
|
||||||
|
command.Description,
|
||||||
|
command.Permission, command.TargetRequired,
|
||||||
|
command.Arguments, Execute, command.SupportedGames);
|
||||||
|
|
||||||
|
manager.RemoveCommandByName(scriptCommand.Name);
|
||||||
|
manager.AddAdditionalCommand(scriptCommand);
|
||||||
|
if (!_registeredCommandNames.Contains(scriptCommand.Name))
|
||||||
|
{
|
||||||
|
_registeredCommandNames.Add(scriptCommand.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetEngineState()
|
||||||
|
{
|
||||||
|
JavascriptEngine oldEngine = null;
|
||||||
|
|
||||||
|
lock (ActiveEngines)
|
||||||
|
{
|
||||||
|
if (ActiveEngines.ContainsKey($"{GetHashCode()}-{_nextEngineId}"))
|
||||||
|
{
|
||||||
|
oldEngine = ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
|
||||||
|
_logger.LogDebug("Removing script engine from active list {HashCode}", _nextEngineId);
|
||||||
|
ActiveEngines.Remove($"{GetHashCode()}-{_nextEngineId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _nextEngineId);
|
||||||
|
oldEngine?.Dispose();
|
||||||
|
var newEngine = new JavascriptEngine(cfg =>
|
||||||
|
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
|
||||||
|
typeof(ScriptPluginExtensions), typeof(LoggerExtensions))
|
||||||
|
.AllowClr(typeof(System.Net.Http.HttpClient).Assembly, typeof(EFClient).Assembly,
|
||||||
|
typeof(Utilities).Assembly, typeof(Encoding).Assembly, typeof(CancellationTokenSource).Assembly,
|
||||||
|
typeof(Data.Models.Client.EFClient).Assembly, typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly, typeof(ScriptPluginWebRequest).Assembly)
|
||||||
|
.CatchClrExceptions()
|
||||||
|
.AddObjectConverter(new EnumsToStringConverter()));
|
||||||
|
|
||||||
|
lock (ActiveEngines)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Adding script engine to active list {HashCode}", _nextEngineId);
|
||||||
|
ActiveEngines.Add($"{GetHashCode()}-{_nextEngineId}", newEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
_scriptPluginConfigurationWrapper =
|
||||||
|
new ScriptPluginConfigurationWrapper(_fileName.Split(Path.DirectorySeparatorChar).Last(), ScriptEngine,
|
||||||
|
_configHandler);
|
||||||
|
|
||||||
|
_scriptPluginConfigurationWrapper.ConfigurationUpdated += (configValue, callbackAction) =>
|
||||||
|
{
|
||||||
|
WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
callbackAction.DynamicInvoke(JsValue.Undefined, new[] { configValue });
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, _logger, _fileName, _onProcessingScript);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterScriptEntities(IManager manager)
|
||||||
|
{
|
||||||
|
foreach (var commandName in _registeredCommandNames)
|
||||||
|
{
|
||||||
|
manager.RemoveCommandByName(commandName);
|
||||||
|
_logger.LogDebug("Unregistered script plugin command {Command} for {Plugin}", commandName, Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredCommandNames.Clear();
|
||||||
|
|
||||||
|
foreach (var interactionName in _registeredInteractions)
|
||||||
|
{
|
||||||
|
_interactionRegistration.UnregisterInteraction(interactionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredInteractions.Clear();
|
||||||
|
|
||||||
|
foreach (var (removeMethod, subscriptions) in _registeredEvents)
|
||||||
|
{
|
||||||
|
foreach (var subscription in subscriptions)
|
||||||
|
{
|
||||||
|
removeMethod.Invoke(null, new[] { subscription });
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredEvents.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EventCallbackWrapper(string eventCallbackName, Delegate javascriptAction)
|
||||||
|
{
|
||||||
|
var eventCategory = eventCallbackName.Split(".")[0];
|
||||||
|
|
||||||
|
var eventCategoryType = eventCategory switch
|
||||||
|
{
|
||||||
|
nameof(IManagementEventSubscriptions) => typeof(IManagementEventSubscriptions),
|
||||||
|
nameof(IGameEventSubscriptions) => typeof(IGameEventSubscriptions),
|
||||||
|
nameof(IGameServerEventSubscriptions) => typeof(IGameServerEventSubscriptions),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (eventCategoryType is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("{EventCategory} is not a valid subscription category", eventCategory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventName = eventCallbackName.Split(".")[1];
|
||||||
|
var eventAddMethod = eventCategoryType.GetMethods()
|
||||||
|
.FirstOrDefault(method => method.Name.StartsWith($"add_{eventName}"));
|
||||||
|
var eventRemoveMethod = eventCategoryType.GetMethods()
|
||||||
|
.FirstOrDefault(method => method.Name.StartsWith($"remove_{eventName}"));
|
||||||
|
|
||||||
|
if (eventAddMethod is null || eventRemoveMethod is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("{EventName} is not a valid subscription event", eventName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var genericType = eventAddMethod.GetParameters()[0].ParameterType.GetGenericArguments()[0];
|
||||||
|
|
||||||
|
var eventWrapper =
|
||||||
|
typeof(ScriptPluginV2).GetMethod(nameof(BuildEventWrapper), BindingFlags.Static | BindingFlags.NonPublic)!
|
||||||
|
.MakeGenericMethod(genericType)
|
||||||
|
.Invoke(null,
|
||||||
|
new object[]
|
||||||
|
{ _logger, _fileName, javascriptAction, GetHashCode(), _nextEngineId, _onProcessingScript });
|
||||||
|
|
||||||
|
eventAddMethod.Invoke(null, new[] { eventWrapper });
|
||||||
|
|
||||||
|
if (!_registeredEvents.ContainsKey(eventRemoveMethod))
|
||||||
|
{
|
||||||
|
_registeredEvents.Add(eventRemoveMethod, new List<object> { eventWrapper });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_registeredEvents[eventRemoveMethod].Add(eventWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<TEventType, CancellationToken, Task> BuildEventWrapper<TEventType>(ILogger logger,
|
||||||
|
string fileName, Delegate javascriptAction, int hashCode, int engineId, SemaphoreSlim onProcessingScript)
|
||||||
|
{
|
||||||
|
return (coreEvent, token) =>
|
||||||
|
{
|
||||||
|
return WrapJavaScriptErrorHandling(() =>
|
||||||
|
{
|
||||||
|
if (IsEngineDisposed(hashCode, engineId))
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
JavascriptEngine engine;
|
||||||
|
|
||||||
|
lock (ActiveEngines)
|
||||||
|
{
|
||||||
|
engine = ActiveEngines[$"{hashCode}-{engineId}"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = new object[] { coreEvent, token }
|
||||||
|
.Select(param => JsValue.FromObject(engine, param))
|
||||||
|
.ToArray();
|
||||||
|
javascriptAction.DynamicInvoke(JsValue.Undefined, args);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, logger, fileName, onProcessingScript, (coreEvent as GameServerEvent)?.Server,
|
||||||
|
additionalData: coreEvent.GetType().Name);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEngineDisposed(int hashCode, int engineId)
|
||||||
|
{
|
||||||
|
lock (ActiveEngines)
|
||||||
|
{
|
||||||
|
return !ActiveEngines.ContainsKey($"{hashCode}-{engineId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TResultType WrapJavaScriptErrorHandling<TResultType>(Func<TResultType> work, ILogger logger,
|
||||||
|
string fileName, SemaphoreSlim onProcessingScript, IGameServer server = null, object additionalData = null,
|
||||||
|
bool throwException = false,
|
||||||
|
[CallerMemberName] string methodName = "")
|
||||||
|
{
|
||||||
|
using (LogContext.PushProperty("Server", server?.Id))
|
||||||
|
{
|
||||||
|
var waitCompleted = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
onProcessingScript.Wait();
|
||||||
|
waitCompleted = true;
|
||||||
|
return work();
|
||||||
|
}
|
||||||
|
catch (JavaScriptException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex,
|
||||||
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
|
||||||
|
methodName, Path.GetFileName(fileName), ex.Location, ex.StackTrace, additionalData);
|
||||||
|
|
||||||
|
if (throwException)
|
||||||
|
{
|
||||||
|
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
|
||||||
|
{
|
||||||
|
logger.LogError(ex,
|
||||||
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
|
||||||
|
methodName, fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
|
||||||
|
|
||||||
|
if (throwException)
|
||||||
|
{
|
||||||
|
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex,
|
||||||
|
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
|
||||||
|
methodName, Path.GetFileName(fileName));
|
||||||
|
|
||||||
|
if (throwException)
|
||||||
|
{
|
||||||
|
throw new PluginException("An error occured while executing action for script plugin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (waitCompleted)
|
||||||
|
{
|
||||||
|
onProcessingScript.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScriptPluginDetails AsScriptPluginInstance(dynamic source)
|
||||||
|
{
|
||||||
|
var commandDetails = ParseScriptCommandDetails(source);
|
||||||
|
|
||||||
|
var interactionDetails = Array.Empty<ScriptPluginInteractionDetails>();
|
||||||
|
if (HasProperty(source, "interactions") && source.interactions is dynamic[])
|
||||||
|
{
|
||||||
|
interactionDetails = ((dynamic[])source.interactions).Select(interaction =>
|
||||||
|
{
|
||||||
|
var name = HasProperty(interaction, "name") && interaction.name is string
|
||||||
|
? (string)interaction.name
|
||||||
|
: string.Empty;
|
||||||
|
var action = HasProperty(interaction, "action") && interaction.action is Delegate
|
||||||
|
? (Delegate)interaction.action
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new ScriptPluginInteractionDetails(name, action);
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty;
|
||||||
|
var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty;
|
||||||
|
var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty;
|
||||||
|
|
||||||
|
return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScriptPluginCommandDetails[] ParseScriptCommandDetails(dynamic source)
|
||||||
|
{
|
||||||
|
var commandDetails = Array.Empty<ScriptPluginCommandDetails>();
|
||||||
|
if (HasProperty(source, "commands") && source.commands is dynamic[])
|
||||||
|
{
|
||||||
|
commandDetails = ((dynamic[])source.commands).Select(command =>
|
||||||
|
{
|
||||||
|
var commandArgs = Array.Empty<CommandArgument>();
|
||||||
|
if (HasProperty(command, "arguments") && command.arguments is dynamic[])
|
||||||
|
{
|
||||||
|
commandArgs = ((dynamic[])command.arguments).Select(argument => new CommandArgument
|
||||||
|
{
|
||||||
|
Name = HasProperty(argument, "name") ? argument.name : string.Empty,
|
||||||
|
Required = HasProperty(argument, "required") && argument.required is bool &&
|
||||||
|
(bool)argument.required
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = HasProperty(command, "name") && command.name is string
|
||||||
|
? (string)command.name
|
||||||
|
: string.Empty;
|
||||||
|
var description = HasProperty(command, "description") && command.description is string
|
||||||
|
? (string)command.description
|
||||||
|
: string.Empty;
|
||||||
|
var alias = HasProperty(command, "alias") && command.alias is string
|
||||||
|
? (string)command.alias
|
||||||
|
: string.Empty;
|
||||||
|
var permission = HasProperty(command, "permission") && command.permission is string
|
||||||
|
? (string)command.permission
|
||||||
|
: string.Empty;
|
||||||
|
var isTargetRequired = HasProperty(command, "targetRequired") && command.targetRequired is bool &&
|
||||||
|
(bool)command.targetRequired;
|
||||||
|
var supportedGames =
|
||||||
|
HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable<object>
|
||||||
|
? ((IEnumerable<object>)command.supportedGames).Where(game => !string.IsNullOrEmpty(game?.ToString()))
|
||||||
|
.Select(game =>
|
||||||
|
Enum.Parse<Reference.Game>(game.ToString()!))
|
||||||
|
: Array.Empty<Reference.Game>();
|
||||||
|
var execute = HasProperty(command, "execute") && command.execute is Delegate
|
||||||
|
? (Delegate)command.execute
|
||||||
|
: (GameEvent _) => Task.CompletedTask;
|
||||||
|
|
||||||
|
return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired,
|
||||||
|
commandArgs, supportedGames, execute);
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasProperty(dynamic source, string name)
|
||||||
|
{
|
||||||
|
Type objType = source.GetType();
|
||||||
|
|
||||||
|
if (objType == typeof(ExpandoObject))
|
||||||
|
{
|
||||||
|
return ((IDictionary<string, object>)source).ContainsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return objType.GetProperty(name) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EnumsToStringConverter : IObjectConverter
|
||||||
|
{
|
||||||
|
public bool TryConvert(Engine engine, object value, out JsValue result)
|
||||||
|
{
|
||||||
|
if (value is Enum)
|
||||||
|
{
|
||||||
|
result = value.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = JsValue.Null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
Application/Plugin/Script/ScriptPluginWebRequest.cs
Normal file
6
Application/Plugin/Script/ScriptPluginWebRequest.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Plugin.Script;
|
||||||
|
|
||||||
|
public record ScriptPluginWebRequest(string Url, object Body = null, string Method = "GET", string ContentType = "text/plain",
|
||||||
|
Dictionary<string, string> Headers = null);
|
297
Application/QueryHelpers/ClientResourceQueryHelper.cs
Normal file
297
Application/QueryHelpers/ClientResourceQueryHelper.cs
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Data.Abstractions;
|
||||||
|
using Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using WebfrontCore.Permissions;
|
||||||
|
using WebfrontCore.QueryHelpers.Models;
|
||||||
|
using EFClient = Data.Models.Client.EFClient;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.QueryHelpers;
|
||||||
|
|
||||||
|
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
|
||||||
|
{
|
||||||
|
public ApplicationConfiguration _appConfig { get; }
|
||||||
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
|
private readonly IGeoLocationService _geoLocationService;
|
||||||
|
|
||||||
|
private class ClientAlias
|
||||||
|
{
|
||||||
|
public EFClient Client { get; set; }
|
||||||
|
public EFAlias Alias { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService,
|
||||||
|
ApplicationConfiguration appConfig)
|
||||||
|
{
|
||||||
|
_appConfig = appConfig;
|
||||||
|
_contextFactory = contextFactory;
|
||||||
|
_geoLocationService = geoLocationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResourceQueryHelperResult<ClientResourceResponse>> QueryResource(ClientResourceRequest query)
|
||||||
|
{
|
||||||
|
await using var context = _contextFactory.CreateContext(false);
|
||||||
|
var iqAliases = context.Aliases.AsQueryable();
|
||||||
|
var iqClients = context.Clients.AsQueryable();
|
||||||
|
|
||||||
|
var iqClientAliases = iqClients.Join(iqAliases, client => client.AliasLinkId, alias => alias.LinkId,
|
||||||
|
(client, alias) => new ClientAlias { Client = client, Alias = alias });
|
||||||
|
|
||||||
|
return await StartFromClient(query, iqClientAliases, iqClients);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ResourceQueryHelperResult<ClientResourceResponse>> StartFromClient(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clientAliases, IQueryable<EFClient> iqClients)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.ClientGuid))
|
||||||
|
{
|
||||||
|
clientAliases = SearchByGuid(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.ClientLevel is not null)
|
||||||
|
{
|
||||||
|
clientAliases = SearchByLevel(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.ClientConnected is not null)
|
||||||
|
{
|
||||||
|
clientAliases = SearchByLastConnection(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.GameName is not null)
|
||||||
|
{
|
||||||
|
clientAliases = SearchByGame(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.ClientName))
|
||||||
|
{
|
||||||
|
clientAliases = SearchByName(query, clientAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.ClientIp))
|
||||||
|
{
|
||||||
|
clientAliases = SearchByIp(query, clientAliases,
|
||||||
|
_appConfig.HasPermission(query.RequesterPermission, WebfrontEntity.ClientIPAddress,
|
||||||
|
WebfrontPermission.Read));
|
||||||
|
}
|
||||||
|
|
||||||
|
var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection });
|
||||||
|
|
||||||
|
iqGroupedClientAliases = query.Direction == SortDirection.Descending
|
||||||
|
? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection)
|
||||||
|
: iqGroupedClientAliases.OrderBy(clientAlias => clientAlias.Key.LastConnection);
|
||||||
|
|
||||||
|
var clientIds = iqGroupedClientAliases.Select(g => g.Key.ClientId)
|
||||||
|
.Skip(query.Offset)
|
||||||
|
.Take(query.Count);
|
||||||
|
|
||||||
|
// this pulls in more records than we need, but it's more efficient than ordering grouped entities
|
||||||
|
var clientLookups = await clientAliases
|
||||||
|
.Where(clientAlias => clientIds.Contains(clientAlias.Client.ClientId))
|
||||||
|
.Select(clientAlias => new ClientResourceResponse
|
||||||
|
{
|
||||||
|
ClientId = clientAlias.Client.ClientId,
|
||||||
|
AliasId = clientAlias.Alias.AliasId,
|
||||||
|
LinkId = clientAlias.Client.AliasLinkId,
|
||||||
|
CurrentClientName = clientAlias.Client.CurrentAlias.Name,
|
||||||
|
MatchedClientName = clientAlias.Alias.Name,
|
||||||
|
CurrentClientIp = clientAlias.Client.CurrentAlias.IPAddress,
|
||||||
|
MatchedClientIp = clientAlias.Alias.IPAddress,
|
||||||
|
ClientLevel = clientAlias.Client.Level.ToLocalizedLevelName(),
|
||||||
|
ClientLevelValue = clientAlias.Client.Level,
|
||||||
|
LastConnection = clientAlias.Client.LastConnection,
|
||||||
|
Game = clientAlias.Client.GameName
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var groupClients = clientLookups.GroupBy(x => x.ClientId);
|
||||||
|
|
||||||
|
var orderedClients = query.Direction == SortDirection.Descending
|
||||||
|
? groupClients.OrderByDescending(SearchByAliasLocal(query.ClientName, query.ClientIp))
|
||||||
|
: groupClients.OrderBy(SearchByAliasLocal(query.ClientName, query.ClientIp));
|
||||||
|
|
||||||
|
var clients = orderedClients.Select(client => client.First()).ToList();
|
||||||
|
await ProcessAliases(query, clients);
|
||||||
|
|
||||||
|
return new ResourceQueryHelperResult<ClientResourceResponse>
|
||||||
|
{
|
||||||
|
Results = clients
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessAliases(ClientResourceRequest query, IEnumerable<ClientResourceResponse> clients)
|
||||||
|
{
|
||||||
|
await Parallel.ForEachAsync(clients, new ParallelOptions { MaxDegreeOfParallelism = 15 },
|
||||||
|
async (client, token) =>
|
||||||
|
{
|
||||||
|
if (!query.IncludeGeolocationData || client.CurrentClientIp is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var geolocationData = await _geoLocationService.Locate(client.CurrentClientIp.ConvertIPtoString());
|
||||||
|
client.ClientCountryCode = geolocationData.CountryCode;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(client.ClientCountryCode))
|
||||||
|
{
|
||||||
|
client.ClientCountryDisplayName = geolocationData.Country;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string clientName,
|
||||||
|
string ipAddress)
|
||||||
|
{
|
||||||
|
return group =>
|
||||||
|
{
|
||||||
|
ClientResourceResponse match = null;
|
||||||
|
var lowercaseClientName = clientName?.ToLower();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(lowercaseClientName))
|
||||||
|
{
|
||||||
|
match = group.ToList().FirstOrDefault(SearchByNameLocal(lowercaseClientName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match is null && !string.IsNullOrWhiteSpace(ipAddress))
|
||||||
|
{
|
||||||
|
match = group.ToList().FirstOrDefault(SearchByIpLocal(ipAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (match ?? group.First()).LastConnection;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<ClientResourceResponse, bool> SearchByNameLocal(string clientName)
|
||||||
|
{
|
||||||
|
return clientResourceResponse =>
|
||||||
|
clientResourceResponse.MatchedClientName.Contains(clientName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<ClientResourceResponse, bool> SearchByIpLocal(string clientIp)
|
||||||
|
{
|
||||||
|
return clientResourceResponse => clientResourceResponse.MatchedClientIp.ConvertIPtoString().Contains(clientIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByName(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clientAliases)
|
||||||
|
{
|
||||||
|
var lowerCaseQueryName = query.ClientName.ToLower();
|
||||||
|
|
||||||
|
clientAliases = clientAliases.Where(query.IsExactClientName
|
||||||
|
? ExactNameMatch(lowerCaseQueryName)
|
||||||
|
: LikeNameMatch(lowerCaseQueryName));
|
||||||
|
|
||||||
|
return clientAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<ClientAlias, bool>> LikeNameMatch(string lowerCaseQueryName)
|
||||||
|
{
|
||||||
|
return clientAlias => EF.Functions.Like(
|
||||||
|
clientAlias.Alias.SearchableName,
|
||||||
|
$"%{lowerCaseQueryName}%") || EF.Functions.Like(
|
||||||
|
clientAlias.Alias.Name.ToLower(),
|
||||||
|
$"%{lowerCaseQueryName}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<ClientAlias, bool>> ExactNameMatch(string lowerCaseQueryName)
|
||||||
|
{
|
||||||
|
return clientAlias =>
|
||||||
|
lowerCaseQueryName == clientAlias.Alias.Name || lowerCaseQueryName == clientAlias.Alias.SearchableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByIp(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clientAliases, bool canSearchIP)
|
||||||
|
{
|
||||||
|
var ipString = query.ClientIp.Trim();
|
||||||
|
var ipAddress = ipString.ConvertToIP();
|
||||||
|
|
||||||
|
if (ipAddress != null && ipString.Split('.').Length == 4 && query.IsExactClientIp)
|
||||||
|
{
|
||||||
|
clientAliases = clientAliases.Where(clientAlias =>
|
||||||
|
clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress);
|
||||||
|
}
|
||||||
|
else if(canSearchIP)
|
||||||
|
{
|
||||||
|
clientAliases = clientAliases.Where(clientAlias =>
|
||||||
|
EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByGuid(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
var guidString = query.ClientGuid.Trim();
|
||||||
|
var parsedGuids = new List<long>();
|
||||||
|
long guid = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
guid = guidString.ConvertGuidToLong(NumberStyles.HexNumber, false, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid != 0)
|
||||||
|
{
|
||||||
|
parsedGuids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
guid = guidString.ConvertGuidToLong(NumberStyles.Integer, false, 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guid != 0)
|
||||||
|
{
|
||||||
|
parsedGuids.Add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedGuids.Any())
|
||||||
|
{
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
clients = clients.Where(client => parsedGuids.Contains(client.Client.NetworkId));
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByLevel(ClientResourceRequest query, IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
clients = clients.Where(clientAlias => clientAlias.Client.Level == query.ClientLevel);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByLastConnection(ClientResourceRequest query,
|
||||||
|
IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
clients = clients.Where(clientAlias => clientAlias.Client.LastConnection >= query.ClientConnected);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ClientAlias> SearchByGame(ClientResourceRequest query, IQueryable<ClientAlias> clients)
|
||||||
|
{
|
||||||
|
clients = clients.Where(clientAlias => clientAlias.Client.GameName == query.GameName);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Models;
|
using Data.Models;
|
||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using static SharedLibraryCore.Server;
|
using static SharedLibraryCore.Server;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
@ -19,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
public class BaseRConParser : IRConParser
|
public class BaseRConParser : IRConParser
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private static string _botIpIndicator = "00000000.";
|
||||||
|
|
||||||
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
|
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
|
||||||
{
|
{
|
||||||
@ -51,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
|
||||||
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
|
||||||
|
|
||||||
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
|
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)?$";
|
||||||
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
|
||||||
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
|
||||||
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
|
||||||
@ -80,6 +82,7 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
|
|
||||||
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
|
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
command = command.FormatMessageForEngine(Configuration);
|
||||||
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
|
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
|
||||||
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
|
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
|
||||||
}
|
}
|
||||||
@ -102,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
lineSplit = Array.Empty<string>();
|
lineSplit = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = string.Join('\n', lineSplit).TrimEnd('\0');
|
var response = string.Join('\n', lineSplit).Replace("\n", "").TrimEnd('\0');
|
||||||
var match = Regex.Match(response, Configuration.Dvar.Pattern);
|
var match = Regex.Match(response, Configuration.Dvar.Pattern);
|
||||||
|
|
||||||
if (response.Contains("Unknown command") ||
|
if (response.Contains("Unknown command") ||
|
||||||
@ -140,6 +143,30 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
|
||||||
|
{
|
||||||
|
if (action.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
callback?.Invoke(new AsyncResult
|
||||||
|
{
|
||||||
|
IsCompleted = true,
|
||||||
|
AsyncState = (true, action.Result.Value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
callback?.Invoke(new AsyncResult
|
||||||
|
{
|
||||||
|
IsCompleted = true,
|
||||||
|
AsyncState = (false, (string)null)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
|
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
|
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
|
||||||
@ -149,16 +176,16 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
return new StatusResponse
|
return new StatusResponse
|
||||||
{
|
{
|
||||||
Clients = ClientsFromStatus(response).ToArray(),
|
Clients = ClientsFromStatus(response).ToArray(),
|
||||||
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
|
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus),
|
||||||
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
|
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus),
|
||||||
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern),
|
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus),
|
||||||
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern)
|
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, string groupPattern)
|
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, ParserRegex parserRegex)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(groupPattern))
|
if (string.IsNullOrEmpty(parserRegex.Pattern))
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
@ -166,11 +193,15 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
string value = null;
|
string value = null;
|
||||||
foreach (var line in response)
|
foreach (var line in response)
|
||||||
{
|
{
|
||||||
var regex = Regex.Match(line, groupPattern);
|
var regex = Regex.Match(line, parserRegex.Pattern);
|
||||||
if (regex.Success)
|
|
||||||
|
if (!regex.Success || !parserRegex.GroupMapping.ContainsKey(groupType))
|
||||||
{
|
{
|
||||||
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == null)
|
if (value == null)
|
||||||
@ -195,6 +226,31 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
|
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
|
||||||
|
{
|
||||||
|
if (action.Exception is null && !action.IsCanceled)
|
||||||
|
{
|
||||||
|
callback?.Invoke(new AsyncResult
|
||||||
|
{
|
||||||
|
IsCompleted = true,
|
||||||
|
AsyncState = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
callback?.Invoke(new AsyncResult
|
||||||
|
{
|
||||||
|
IsCompleted = true,
|
||||||
|
AsyncState = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
private List<EFClient> ClientsFromStatus(string[] Status)
|
private List<EFClient> ClientsFromStatus(string[] Status)
|
||||||
{
|
{
|
||||||
List<EFClient> StatusPlayers = new List<EFClient>();
|
List<EFClient> StatusPlayers = new List<EFClient>();
|
||||||
@ -239,13 +295,20 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
long networkId;
|
long networkId;
|
||||||
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
|
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
|
||||||
string networkIdString;
|
string networkIdString;
|
||||||
|
|
||||||
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
|
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
|
||||||
|
|
||||||
|
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
|
||||||
|
.Contains(_botIpIndicator))
|
||||||
|
{
|
||||||
|
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
|
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
|
||||||
|
|
||||||
networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ?
|
networkId = networkIdString.IsBotGuid() || (ip == null && ping is 999 or 0) ?
|
||||||
name.GenerateGuidFromString() :
|
name.GenerateGuidFromString() :
|
||||||
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||||
}
|
}
|
||||||
@ -255,9 +318,9 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = new EFClient()
|
var client = new EFClient
|
||||||
{
|
{
|
||||||
CurrentAlias = new EFAlias()
|
CurrentAlias = new EFAlias
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
IPAddress = ip
|
IPAddress = ip
|
||||||
@ -309,15 +372,28 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
|
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
|
||||||
default;
|
default;
|
||||||
|
|
||||||
public TimeSpan OverrideTimeoutForCommand(string command)
|
public TimeSpan? OverrideTimeoutForCommand(string command)
|
||||||
{
|
{
|
||||||
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
|
if (string.IsNullOrEmpty(command))
|
||||||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
{
|
||||||
return TimeSpan.FromSeconds(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TimeSpan.Zero;
|
return TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
|
||||||
|
|
||||||
|
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
|
||||||
|
{
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
|
||||||
|
|
||||||
|
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
|
||||||
|
{
|
||||||
|
return TimeSpan.FromSeconds(timeoutValue.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,14 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
|
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
|
||||||
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
|
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
|
||||||
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
|
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
|
||||||
|
public IDictionary<string, int?> OverrideCommandTimeouts { get; set; } = new Dictionary<string, int?>();
|
||||||
public int NoticeMaximumLines { get; set; } = 8;
|
public int NoticeMaximumLines { get; set; } = 8;
|
||||||
public int NoticeMaxCharactersPerLine { get; set; } = 50;
|
public int NoticeMaxCharactersPerLine { get; set; } = 50;
|
||||||
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
|
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
|
||||||
public int? DefaultRConPort { get; set; }
|
public int? DefaultRConPort { get; set; }
|
||||||
public string DefaultInstallationDirectoryHint { get; set; }
|
public string DefaultInstallationDirectoryHint { get; set; }
|
||||||
public short FloodProtectInterval { get; set; } = 750;
|
public short FloodProtectInterval { get; set; } = 750;
|
||||||
|
public bool ShouldRemoveDiacritics { get; set; }
|
||||||
|
|
||||||
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
|
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
|
||||||
{
|
{
|
||||||
@ -46,7 +48,7 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
{ColorCodes.White.ToString(), "^7"},
|
{ColorCodes.White.ToString(), "^7"},
|
||||||
{ColorCodes.Map.ToString(), "^8"},
|
{ColorCodes.Map.ToString(), "^8"},
|
||||||
{ColorCodes.Grey.ToString(), "^9"},
|
{ColorCodes.Grey.ToString(), "^9"},
|
||||||
{ColorCodes.Wildcard.ToString(), ":^"},
|
{ColorCodes.Wildcard.ToString(), "^:"}
|
||||||
};
|
};
|
||||||
|
|
||||||
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
|
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
|
||||||
@ -58,6 +60,25 @@ namespace IW4MAdmin.Application.RConParsers
|
|||||||
StatusHeader = parserRegexFactory.CreateParserRegex();
|
StatusHeader = parserRegexFactory.CreateParserRegex();
|
||||||
HostnameStatus = parserRegexFactory.CreateParserRegex();
|
HostnameStatus = parserRegexFactory.CreateParserRegex();
|
||||||
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
|
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
|
||||||
|
|
||||||
|
|
||||||
|
const string mapRotateCommand = "map_rotate";
|
||||||
|
const string mapCommand = "map";
|
||||||
|
const string fastRestartCommand = "fast_restart";
|
||||||
|
const string quitCommand = "quit";
|
||||||
|
|
||||||
|
foreach (var command in new[] { mapRotateCommand, mapCommand, fastRestartCommand})
|
||||||
|
{
|
||||||
|
if (!OverrideCommandTimeouts.ContainsKey(command))
|
||||||
|
{
|
||||||
|
OverrideCommandTimeouts.Add(command, 45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OverrideCommandTimeouts.ContainsKey(quitCommand))
|
||||||
|
{
|
||||||
|
OverrideCommandTimeouts.Add(quitCommand, 0); // we don't want to wait for a response when we quit the server
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Events;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
|
||||||
{
|
|
||||||
class SerialGameEventHandler : IEventHandler
|
|
||||||
{
|
|
||||||
private delegate void GameEventAddedEventHandler(object sender, GameEventArgs args);
|
|
||||||
private event GameEventAddedEventHandler GameEventAdded;
|
|
||||||
|
|
||||||
private static readonly GameEvent.EventType[] overrideEvents = new[]
|
|
||||||
{
|
|
||||||
GameEvent.EventType.Connect,
|
|
||||||
GameEvent.EventType.Disconnect,
|
|
||||||
GameEvent.EventType.Quit,
|
|
||||||
GameEvent.EventType.Stop
|
|
||||||
};
|
|
||||||
|
|
||||||
public SerialGameEventHandler()
|
|
||||||
{
|
|
||||||
GameEventAdded += GameEventHandler_GameEventAdded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args)
|
|
||||||
{
|
|
||||||
await (sender as IManager).ExecuteEvent(args.Event);
|
|
||||||
EventApi.OnGameEvent(args.Event);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void HandleEvent(IManager manager, GameEvent gameEvent)
|
|
||||||
{
|
|
||||||
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
|
|
||||||
{
|
|
||||||
GameEventAdded?.Invoke(manager, new GameEventArgs(null, false, gameEvent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -9,6 +10,12 @@ namespace Data.Abstractions
|
|||||||
{
|
{
|
||||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||||
TimeSpan? expirationTime = null, bool autoRefresh = false);
|
TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||||
|
|
||||||
|
void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||||
|
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||||
|
|
||||||
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
|
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
|
||||||
|
|
||||||
|
Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -32,6 +32,7 @@ namespace Data.Context
|
|||||||
public DbSet<EFClientMessage> ClientMessages { get; set; }
|
public DbSet<EFClientMessage> ClientMessages { get; set; }
|
||||||
|
|
||||||
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
|
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
|
||||||
|
public DbSet<EFClientStatistics> ClientStatistics { get; set; }
|
||||||
public DbSet<EFHitLocation> HitLocations { get; set; }
|
public DbSet<EFHitLocation> HitLocations { get; set; }
|
||||||
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
|
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
|
||||||
public DbSet<EFWeapon> Weapons { get; set; }
|
public DbSet<EFWeapon> Weapons { get; set; }
|
||||||
@ -85,7 +86,16 @@ namespace Data.Context
|
|||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
// make network id unique
|
// make network id unique
|
||||||
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); });
|
modelBuilder.Entity<EFClient>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasIndex(client => client.NetworkId);
|
||||||
|
entity.HasIndex(client => client.LastConnection);
|
||||||
|
entity.HasAlternateKey(client => new
|
||||||
|
{
|
||||||
|
client.NetworkId,
|
||||||
|
client.GameName
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<EFPenalty>(entity =>
|
modelBuilder.Entity<EFPenalty>(entity =>
|
||||||
{
|
{
|
||||||
@ -121,6 +131,7 @@ namespace Data.Context
|
|||||||
ent.HasIndex(_alias => _alias.SearchableName);
|
ent.HasIndex(_alias => _alias.SearchableName);
|
||||||
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress});
|
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress});
|
||||||
ent.Property(alias => alias.SearchableIPAddress)
|
ent.Property(alias => alias.SearchableIPAddress)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasComputedColumnSql(@"((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", stored: true);
|
.HasComputedColumnSql(@"((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", stored: true);
|
||||||
ent.HasIndex(alias => alias.SearchableIPAddress);
|
ent.HasIndex(alias => alias.SearchableIPAddress);
|
||||||
});
|
});
|
||||||
@ -142,6 +153,8 @@ namespace Data.Context
|
|||||||
|
|
||||||
modelBuilder.Entity<EFClientConnectionHistory>(ent => ent.HasIndex(history => history.CreatedDateTime));
|
modelBuilder.Entity<EFClientConnectionHistory>(ent => ent.HasIndex(history => history.CreatedDateTime));
|
||||||
|
|
||||||
|
modelBuilder.Entity<EFServerSnapshot>(ent => ent.HasIndex(snapshot => snapshot.CapturedAt));
|
||||||
|
|
||||||
// force full name for database conversion
|
// force full name for database conversion
|
||||||
modelBuilder.Entity<EFClient>().ToTable("EFClients");
|
modelBuilder.Entity<EFClient>().ToTable("EFClients");
|
||||||
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");
|
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Abstractions;
|
using Data.Abstractions;
|
||||||
@ -15,8 +17,8 @@ namespace Data.Helpers
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
|
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
|
||||||
new ConcurrentDictionary<string, CacheState<TReturnType>>();
|
private readonly string _defaultKey = null;
|
||||||
|
|
||||||
private bool _autoRefresh;
|
private bool _autoRefresh;
|
||||||
private const int DefaultExpireMinutes = 15;
|
private const int DefaultExpireMinutes = 15;
|
||||||
@ -27,7 +29,7 @@ namespace Data.Helpers
|
|||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public DateTime LastRetrieval { get; set; }
|
public DateTime LastRetrieval { get; set; }
|
||||||
public TimeSpan ExpirationTime { get; set; }
|
public TimeSpan ExpirationTime { get; set; }
|
||||||
public Func<DbSet<TEntityType>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
public Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
||||||
public TCacheType Value { get; set; }
|
public TCacheType Value { get; set; }
|
||||||
public bool IsSet { get; set; }
|
public bool IsSet { get; set; }
|
||||||
|
|
||||||
@ -51,11 +53,29 @@ namespace Data.Helpers
|
|||||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||||
TimeSpan? expirationTime = null, bool autoRefresh = false)
|
TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||||
{
|
{
|
||||||
if (_cacheStates.ContainsKey(key))
|
SetCacheItem((set, _, token) => getter(set, token), key, null, expirationTime, autoRefresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||||
|
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||||
|
{
|
||||||
|
ids ??= new[] { _defaultKey };
|
||||||
|
|
||||||
|
if (!_cacheStates.ContainsKey(key))
|
||||||
|
{
|
||||||
|
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheInstance = _cacheStates[key];
|
||||||
|
var id = GenerateKeyFromIds(ids);
|
||||||
|
|
||||||
|
lock (_cacheStates)
|
||||||
|
{
|
||||||
|
if (cacheInstance.ContainsKey(id))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Cache key {Key} is already added", key);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var state = new CacheState<TReturnType>
|
var state = new CacheState<TReturnType>
|
||||||
{
|
{
|
||||||
@ -64,9 +84,12 @@ namespace Data.Helpers
|
|||||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||||
};
|
};
|
||||||
|
|
||||||
_autoRefresh = autoRefresh;
|
lock (_cacheStates)
|
||||||
|
{
|
||||||
|
cacheInstance.Add(id, state);
|
||||||
|
}
|
||||||
|
|
||||||
_cacheStates.TryAdd(key, state);
|
_autoRefresh = autoRefresh;
|
||||||
|
|
||||||
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
||||||
{
|
{
|
||||||
@ -74,37 +97,48 @@ namespace Data.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, ids, CancellationToken.None);
|
||||||
_timer.Start();
|
_timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
|
public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
||||||
|
GetCacheItem(keyName, null, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!_cacheStates.ContainsKey(keyName))
|
if (!_cacheStates.ContainsKey(keyName))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("No cache found for key {key}", keyName);
|
throw new ArgumentException("No cache found for key {key}", keyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = _cacheStates[keyName];
|
var cacheInstance = _cacheStates[keyName];
|
||||||
|
|
||||||
|
CacheState<TReturnType> state;
|
||||||
|
|
||||||
|
lock (_cacheStates)
|
||||||
|
{
|
||||||
|
state = ids is null ? cacheInstance.Values.First() : _cacheStates[keyName][GenerateKeyFromIds(ids)];
|
||||||
|
}
|
||||||
|
|
||||||
// when auto refresh is off we want to check the expiration and value
|
// when auto refresh is off we want to check the expiration and value
|
||||||
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
|
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
|
||||||
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
|
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
|
||||||
{
|
{
|
||||||
await RunCacheUpdate(state, cancellationToken);
|
await RunCacheUpdate(state, ids, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.Value;
|
return state.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunCacheUpdate(CacheState<TReturnType> state, CancellationToken token)
|
private async Task RunCacheUpdate(CacheState<TReturnType> state, IEnumerable<object> ids, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
|
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
|
||||||
await using var context = _contextFactory.CreateContext(false);
|
await using var context = _contextFactory.CreateContext(false);
|
||||||
var set = context.Set<TEntityType>();
|
var set = context.Set<TEntityType>();
|
||||||
var value = await state.Getter(set, token);
|
var value = await state.Getter(set, ids, token);
|
||||||
state.Value = value;
|
state.Value = value;
|
||||||
state.IsSet = true;
|
state.IsSet = true;
|
||||||
state.LastRetrieval = DateTime.Now;
|
state.LastRetrieval = DateTime.Now;
|
||||||
@ -114,5 +148,8 @@ namespace Data.Helpers
|
|||||||
_logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
|
_logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GenerateKeyFromIds(IEnumerable<object> ids) =>
|
||||||
|
string.Join("_", ids.Select(id => id?.ToString() ?? "null"));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,14 +8,14 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace Data.Helpers
|
namespace Data.Helpers;
|
||||||
{
|
|
||||||
public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
|
public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private Dictionary<long, T> _cachedItems;
|
private Dictionary<long, T> _cachedItems;
|
||||||
private readonly SemaphoreSlim _onOperation = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _onOperation = new(1, 1);
|
||||||
|
|
||||||
public LookupCache(ILogger<LookupCache<T>> logger, IDatabaseContextFactory contextFactory)
|
public LookupCache(ILogger<LookupCache<T>> logger, IDatabaseContextFactory contextFactory)
|
||||||
{
|
{
|
||||||
@ -35,7 +35,7 @@ namespace Data.Helpers
|
|||||||
|
|
||||||
if (existingItem != null)
|
if (existingItem != null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Cached item already added for {type} {id} {value}", typeof(T).Name, item.Id,
|
_logger.LogDebug("Cached item already added for {Type} {Id} {Value}", typeof(T).Name, item.Id,
|
||||||
item.Value);
|
item.Value);
|
||||||
_onOperation.Release();
|
_onOperation.Release();
|
||||||
return existingItem;
|
return existingItem;
|
||||||
@ -43,7 +43,7 @@ namespace Data.Helpers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Adding new {type} with {id} {value}", typeof(T).Name, item.Id, item.Value);
|
_logger.LogDebug("Adding new {Type} with {Id} {Value}", typeof(T).Name, item.Id, item.Value);
|
||||||
await using var context = _contextFactory.CreateContext();
|
await using var context = _contextFactory.CreateContext();
|
||||||
context.Set<T>().Add(item);
|
context.Set<T>().Add(item);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
@ -52,7 +52,7 @@ namespace Data.Helpers
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Could not add item to cache for {type}", typeof(T).Name);
|
_logger.LogError(ex, "Could not add item to cache for {Type}", typeof(T).Name);
|
||||||
throw new Exception("Could not add item to cache");
|
throw new Exception("Could not add item to cache");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -66,22 +66,12 @@ namespace Data.Helpers
|
|||||||
|
|
||||||
public async Task<T> FirstAsync(Func<T, bool> query)
|
public async Task<T> FirstAsync(Func<T, bool> query)
|
||||||
{
|
{
|
||||||
await _onOperation.WaitAsync();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await _onOperation.WaitAsync();
|
||||||
var cachedResult = _cachedItems.Values.Where(query);
|
var cachedResult = _cachedItems.Values.Where(query);
|
||||||
|
|
||||||
if (cachedResult.Any())
|
|
||||||
{
|
|
||||||
return cachedResult.FirstOrDefault();
|
return cachedResult.FirstOrDefault();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (_onOperation.CurrentCount == 0)
|
if (_onOperation.CurrentCount == 0)
|
||||||
@ -89,8 +79,6 @@ namespace Data.Helpers
|
|||||||
_onOperation.Release(1);
|
_onOperation.Release(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<T> GetAll()
|
public IEnumerable<T> GetAll()
|
||||||
@ -107,8 +95,7 @@ namespace Data.Helpers
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Could not initialize caching for {cacheType}", typeof(T).Name);
|
_logger.LogError(ex, "Could not initialize caching for {CacheType}", typeof(T).Name);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -808,7 +808,8 @@ namespace Data.Migrations.MySql
|
|||||||
|
|
||||||
b.Property<string>("SearchableIPAddress")
|
b.Property<string>("SearchableIPAddress")
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)")
|
||||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||||
|
|
||||||
b.Property<string>("SearchableName")
|
b.Property<string>("SearchableName")
|
||||||
|
@ -11,7 +11,8 @@ namespace Data.Migrations.MySql
|
|||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
name: "SearchableIPAddress",
|
name: "SearchableIPAddress",
|
||||||
table: "EFAlias",
|
table: "EFAlias",
|
||||||
type: "longtext",
|
type: "varchar(255)",
|
||||||
|
maxLength: 255,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
computedColumnSql: "CONCAT((IPAddress & 255), \".\", ((IPAddress >> 8) & 255), \".\", ((IPAddress >> 16) & 255), \".\", ((IPAddress >> 24) & 255))",
|
computedColumnSql: "CONCAT((IPAddress & 255), \".\", ((IPAddress >> 8) & 255), \".\", ((IPAddress >> 16) & 255), \".\", ((IPAddress >> 24) & 255))",
|
||||||
stored: true)
|
stored: true)
|
||||||
|
@ -808,6 +808,7 @@ namespace Data.Migrations.MySql
|
|||||||
|
|
||||||
b.Property<string>("SearchableIPAddress")
|
b.Property<string>("SearchableIPAddress")
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasColumnType("varchar(255)")
|
.HasColumnType("varchar(255)")
|
||||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||||
|
|
||||||
|
1636
Data/Migrations/MySql/20220422202702_AddGameToEFClient.Designer.cs
generated
Normal file
1636
Data/Migrations/MySql/20220422202702_AddGameToEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Data/Migrations/MySql/20220422202702_AddGameToEFClient.cs
Normal file
25
Data/Migrations/MySql/20220422202702_AddGameToEFClient.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddGameToEFClient : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1638
Data/Migrations/MySql/20220609135128_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1638
Data/Migrations/MySql/20220609135128_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||||
|
table: "EFClientRankingHistory",
|
||||||
|
column: "CreatedDateTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||||
|
table: "EFClientRankingHistory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddAlternateKeyToEFClients : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("UPDATE `EFClients` set `GameName` = 0 WHERE `GameName` IS NULL");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "int",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "AK_EFClients_NetworkId_GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
columns: new[] { "NetworkId", "GameName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "NetworkId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropUniqueConstraint(
|
||||||
|
name: "AK_EFClients_NetworkId_GameName",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "int",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "int");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "NetworkId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on EFClientMessages (TimeSent desc);");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on efclientmessages (TimeSent desc);");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"drop index IX_EFClientMessages_TimeSentDesc on EFClientMessages;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1641
Data/Migrations/MySql/20230124030137_AddLastConnectionIndexEFClient.Designer.cs
generated
Normal file
1641
Data/Migrations/MySql/20230124030137_AddLastConnectionIndexEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddLastConnectionIndexEFClient : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_LastConnection",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "LastConnection");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_LastConnection",
|
||||||
|
table: "EFClients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1644
Data/Migrations/MySql/20230705133025_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
1644
Data/Migrations/MySql/20230705133025_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.MySql
|
||||||
|
{
|
||||||
|
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFServerSnapshot_CapturedAt",
|
||||||
|
table: "EFServerSnapshot",
|
||||||
|
column: "CapturedAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFServerSnapshot_CapturedAt",
|
||||||
|
table: "EFServerSnapshot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -64,6 +64,9 @@ namespace Data.Migrations.MySql
|
|||||||
b.Property<DateTime>("FirstConnection")
|
b.Property<DateTime>("FirstConnection")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("GameName")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime>("LastConnection")
|
b.Property<DateTime>("LastConnection")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
@ -87,12 +90,15 @@ namespace Data.Migrations.MySql
|
|||||||
|
|
||||||
b.HasKey("ClientId");
|
b.HasKey("ClientId");
|
||||||
|
|
||||||
|
b.HasAlternateKey("NetworkId", "GameName");
|
||||||
|
|
||||||
b.HasIndex("AliasLinkId");
|
b.HasIndex("AliasLinkId");
|
||||||
|
|
||||||
b.HasIndex("CurrentAliasId");
|
b.HasIndex("CurrentAliasId");
|
||||||
|
|
||||||
b.HasIndex("NetworkId")
|
b.HasIndex("LastConnection");
|
||||||
.IsUnique();
|
|
||||||
|
b.HasIndex("NetworkId");
|
||||||
|
|
||||||
b.ToTable("EFClients", (string)null);
|
b.ToTable("EFClients", (string)null);
|
||||||
});
|
});
|
||||||
@ -453,6 +459,8 @@ namespace Data.Migrations.MySql
|
|||||||
|
|
||||||
b.HasIndex("ClientId");
|
b.HasIndex("ClientId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedDateTime");
|
||||||
|
|
||||||
b.HasIndex("Ranking");
|
b.HasIndex("Ranking");
|
||||||
|
|
||||||
b.HasIndex("ServerId");
|
b.HasIndex("ServerId");
|
||||||
@ -806,6 +814,7 @@ namespace Data.Migrations.MySql
|
|||||||
|
|
||||||
b.Property<string>("SearchableIPAddress")
|
b.Property<string>("SearchableIPAddress")
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasColumnType("varchar(255)")
|
.HasColumnType("varchar(255)")
|
||||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||||
|
|
||||||
@ -1102,6 +1111,8 @@ namespace Data.Migrations.MySql
|
|||||||
|
|
||||||
b.HasKey("ServerSnapshotId");
|
b.HasKey("ServerSnapshotId");
|
||||||
|
|
||||||
|
b.HasIndex("CapturedAt");
|
||||||
|
|
||||||
b.HasIndex("MapId");
|
b.HasIndex("MapId");
|
||||||
|
|
||||||
b.HasIndex("ServerId");
|
b.HasIndex("ServerId");
|
||||||
|
1693
Data/Migrations/Postgresql/20220422203121_AddGameToEFClient.Designer.cs
generated
Normal file
1693
Data/Migrations/Postgresql/20220422203121_AddGameToEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddGameToEFClient : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1695
Data/Migrations/Postgresql/20220609135210_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1695
Data/Migrations/Postgresql/20220609135210_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||||
|
table: "EFClientRankingHistory",
|
||||||
|
column: "CreatedDateTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||||
|
table: "EFClientRankingHistory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddAlternateKeyToEFClients : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("UPDATE \"EFClients\" SET \"GameName\" = 0 WHERE \"GameName\" IS NULL");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "AK_EFClients_NetworkId_GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
columns: new[] { "NetworkId", "GameName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "NetworkId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropUniqueConstraint(
|
||||||
|
name: "AK_EFClients_NetworkId_GameName",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "NetworkId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
@"CREATE INDEX""IX_EFClientMessages_TimeSentDesc""
|
||||||
|
ON public.""EFClientMessages"" USING btree
|
||||||
|
(""TimeSent"" DESC NULLS LAST)
|
||||||
|
TABLESPACE pg_default;"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(@"DROP INDEX public.""IX_EFClientMessages_TimeSentDesc""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1698
Data/Migrations/Postgresql/20230124030240_AddLastConnectionIndexEFClient.Designer.cs
generated
Normal file
1698
Data/Migrations/Postgresql/20230124030240_AddLastConnectionIndexEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddLastConnectionIndexEFClient : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_LastConnection",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "LastConnection");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_LastConnection",
|
||||||
|
table: "EFClients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1701
Data/Migrations/Postgresql/20230705133135_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
1701
Data/Migrations/Postgresql/20230705133135_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Postgresql
|
||||||
|
{
|
||||||
|
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "SearchableIPAddress",
|
||||||
|
table: "EFAlias",
|
||||||
|
type: "character varying(255)",
|
||||||
|
maxLength: 255,
|
||||||
|
nullable: true,
|
||||||
|
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
|
||||||
|
stored: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldNullable: true,
|
||||||
|
oldComputedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
|
||||||
|
oldStored: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFServerSnapshot_CapturedAt",
|
||||||
|
table: "EFServerSnapshot",
|
||||||
|
column: "CapturedAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFServerSnapshot_CapturedAt",
|
||||||
|
table: "EFServerSnapshot");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "SearchableIPAddress",
|
||||||
|
table: "EFAlias",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
|
||||||
|
stored: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(255)",
|
||||||
|
oldMaxLength: 255,
|
||||||
|
oldNullable: true,
|
||||||
|
oldComputedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
|
||||||
|
oldStored: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,9 @@ namespace Data.Migrations.Postgresql
|
|||||||
b.Property<DateTime>("FirstConnection")
|
b.Property<DateTime>("FirstConnection")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
|
b.Property<int>("GameName")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<DateTime>("LastConnection")
|
b.Property<DateTime>("LastConnection")
|
||||||
.HasColumnType("timestamp without time zone");
|
.HasColumnType("timestamp without time zone");
|
||||||
|
|
||||||
@ -94,12 +97,15 @@ namespace Data.Migrations.Postgresql
|
|||||||
|
|
||||||
b.HasKey("ClientId");
|
b.HasKey("ClientId");
|
||||||
|
|
||||||
|
b.HasAlternateKey("NetworkId", "GameName");
|
||||||
|
|
||||||
b.HasIndex("AliasLinkId");
|
b.HasIndex("AliasLinkId");
|
||||||
|
|
||||||
b.HasIndex("CurrentAliasId");
|
b.HasIndex("CurrentAliasId");
|
||||||
|
|
||||||
b.HasIndex("NetworkId")
|
b.HasIndex("LastConnection");
|
||||||
.IsUnique();
|
|
||||||
|
b.HasIndex("NetworkId");
|
||||||
|
|
||||||
b.ToTable("EFClients", (string)null);
|
b.ToTable("EFClients", (string)null);
|
||||||
});
|
});
|
||||||
@ -472,6 +478,8 @@ namespace Data.Migrations.Postgresql
|
|||||||
|
|
||||||
b.HasIndex("ClientId");
|
b.HasIndex("ClientId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedDateTime");
|
||||||
|
|
||||||
b.HasIndex("Ranking");
|
b.HasIndex("Ranking");
|
||||||
|
|
||||||
b.HasIndex("ServerId");
|
b.HasIndex("ServerId");
|
||||||
@ -845,7 +853,8 @@ namespace Data.Migrations.Postgresql
|
|||||||
|
|
||||||
b.Property<string>("SearchableIPAddress")
|
b.Property<string>("SearchableIPAddress")
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
.HasColumnType("text")
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)")
|
||||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||||
|
|
||||||
b.Property<string>("SearchableName")
|
b.Property<string>("SearchableName")
|
||||||
@ -1155,6 +1164,8 @@ namespace Data.Migrations.Postgresql
|
|||||||
|
|
||||||
b.HasKey("ServerSnapshotId");
|
b.HasKey("ServerSnapshotId");
|
||||||
|
|
||||||
|
b.HasIndex("CapturedAt");
|
||||||
|
|
||||||
b.HasIndex("MapId");
|
b.HasIndex("MapId");
|
||||||
|
|
||||||
b.HasIndex("ServerId");
|
b.HasIndex("ServerId");
|
||||||
|
1634
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.Designer.cs
generated
Normal file
1634
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.cs
Normal file
25
Data/Migrations/Sqlite/20220422202315_AddGameToEFClient.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class AddGameToEFClient : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1636
Data/Migrations/Sqlite/20220609022511_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1636
Data/Migrations/Sqlite/20220609022511_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||||
|
table: "EFClientRankingHistory",
|
||||||
|
column: "CreatedDateTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||||
|
table: "EFClientRankingHistory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class AddAlternateKeyToEFClients : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "INTEGER",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "AK_EFClients_NetworkId_GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
columns: new[] { "NetworkId", "GameName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "NetworkId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropUniqueConstraint(
|
||||||
|
name: "AK_EFClients_NetworkId_GameName",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "GameName",
|
||||||
|
table: "EFClients",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "INTEGER");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_NetworkId",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "NetworkId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1639
Data/Migrations/Sqlite/20230124025802_AddLastConnectionIndexEFClient.Designer.cs
generated
Normal file
1639
Data/Migrations/Sqlite/20230124025802_AddLastConnectionIndexEFClient.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.Migrations.Sqlite
|
||||||
|
{
|
||||||
|
public partial class AddLastConnectionIndexEFClient : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_EFClients_LastConnection",
|
||||||
|
table: "EFClients",
|
||||||
|
column: "LastConnection");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_EFClients_LastConnection",
|
||||||
|
table: "EFClients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1642
Data/Migrations/Sqlite/20230705132822_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
1642
Data/Migrations/Sqlite/20230705132822_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user