Compare commits

...

87 Commits

Author SHA1 Message Date
HGM
710382d432 Update DefaultSettings.json (#282)
Update IW4x Map Names for "Modern Warfare 3 DLC Pack"
2023-03-23 13:04:09 -05:00
Edo
b258d51863 fix(boiii): workaround the goofiest bug (#284)
* fix(boiii): workaround the goofiest bug
2023-03-23 13:03:54 -05:00
782201b086 feature(script_plugins): boiii parser 2023-03-16 10:48:14 -05:00
8fc47ec6c4 fix edge case for temp mute penalties with no expiration 2023-01-24 14:43:00 -06:00
12e3fd9238 fix permissions issue with search 2023-01-24 14:32:48 -06:00
6edf3f1ae9 fix issue with default date and default order on advanced search 2023-01-23 21:23:02 -06:00
8f20a2e2cd add index to last connection for improved search speed 2023-01-23 21:10:33 -06:00
b002991686 update BuildWebCompiler to support newer SCSS functions 2023-01-23 18:33:46 -06:00
ba40478d11 add "advanced" search functionality 2023-01-23 16:38:16 -06:00
c89314667c Update IW5 gametype names. (#240) 2023-01-11 09:05:34 -06:00
b14d7b6865 remove reference to deprecated httpGet in customcallbacks 2023-01-09 13:44:41 -06:00
dabad54872 add more ported iw4x maps to default settings 2023-01-06 18:03:17 -06:00
74e792bfdc merge 2023-01-06 13:45:25 -06:00
eac8483885 temporarily disable plugin interactions 2023-01-06 13:42:38 -06:00
0ebd582532 add new ported cod4 maps to iw4x map list 2023-01-06 13:39:15 -06:00
31e3e98d06 update h1 parser for chat localization 2023-01-06 12:25:24 -06:00
9ef189d303 update iw6x chat localize text 2023-01-06 10:49:10 -06:00
ae101f1c3a fix for iw6x and s1x parser 2023-01-05 21:43:57 -06:00
ef5e36b224 add game name to dropdown list on web console 2022-12-22 19:37:56 -06:00
d0f72390fb fix hidden text for password protected servers on chat context 2022-12-22 19:28:59 -06:00
Edo
8bcc9354fd Update H1 parser too based on iw6&s1 experience 2022-11-07 08:48:38 -06:00
0674ef800b fix(ParserIW6x): Filter out say/say_team correctly 2022-11-07 08:48:38 -06:00
1349cf84b7 properly set the localize text char for s1x parser 2022-11-03 20:11:05 -05:00
b311ecefc2 feature(parser): Option to override special localize character 2022-11-03 20:05:59 -05:00
16739ce455 misc fixes 2022-10-25 15:39:49 -05:00
b5b01cba4c improve webfront command error feedback 2022-10-25 14:52:12 -05:00
797642f3e6 only titleize single word titles on action dialogs 2022-10-25 14:03:35 -05:00
6fa15d3dcc don't intercept commands for login plugin if they are from webfront 2022-10-25 13:22:33 -05:00
7d6bf88bfd Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-10-24 21:15:29 -05:00
2e149ddafd fix profile issue with no available interactions 2022-10-24 21:11:00 -05:00
a16986f7a3 Mute Banner for Profile & Prevent Self-Target & Correctly Expire Early Unmutes (#272)
* Fix self-targeting
Remove creation of penalty on mute expiration

* Display mute penalties on profile
Expire mute penalties on unmute

* Resolves issues in code review
Added comment in ClientController.cs
Fixed order of operations in MuteManager.cs
Fixed condition in MuteManager.cs

* Fix self-targeting
Remove creation of penalty on mute expiration

* Display mute penalties on profile
Expire mute penalties on unmute

* Resolves issues in code review
Added comment in ClientController.cs
Fixed order of operations in MuteManager.cs
Fixed condition in MuteManager.cs

* Changed localisation value to be more generic
Fix null reference warning (it should never be null) (34da216)
2022-10-24 18:58:12 -05:00
dbca3675ba add unban subnet command and subnet list interaction 2022-10-24 18:57:35 -05:00
973ea83ab9 fix issue with random concurrency issue on interaction reaction 2022-10-24 18:57:35 -05:00
69cb4bf9df clean up some repeated script plugin error handling 2022-10-24 18:57:35 -05:00
9a08997825 remove unused method in shared integration 2022-10-23 14:40:14 -05:00
9cf91d030d fix indentation on shared integration 2022-10-23 14:39:39 -05:00
c06b0982a7 cleanup and simplify the CoD RCon implementation 2022-10-23 14:03:57 -05:00
f4e7d5daf9 harden up the script timer/game interface dvar operations for multithreading 2022-10-23 14:03:33 -05:00
f6b3eb04f2 track match start/end time where possible 2022-10-23 13:32:09 -05:00
565f22b42e create shared integration for performance-based autobalance support 2022-10-23 13:29:01 -05:00
7c1c2e719b order permission changed query helper properly 2022-10-21 20:28:04 -05:00
f50d067c73 hide annoying warning 2022-10-18 09:38:54 -05:00
a3fa5212f5 attempt at resolving game interface threading issues (maybe) 2022-10-17 10:45:42 -05:00
12357fd9f7 Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-10-17 09:18:06 -05:00
3367c5c22f add support for plugin generated pages (interactions). add disallow vpn command 2022-10-17 09:17:43 -05:00
3295315339 update default permissions for guest webfront users 2022-10-16 16:25:09 -05:00
cf51b83cdd Fix Threading Duplicate for Mute Penalty & Added !MuteInfo & Fix PM (#269)
* Resolve duplicate migration
Resolve unmuting state double penalties

* Change order of operation

* Added MuteInfoCommand.cs

* Resolve !pm and @broadcast permanently being disabled
2022-10-14 08:47:01 -05:00
76925a78d4 possible improvements for game interface rcon operations 2022-10-13 13:53:28 -05:00
7b869a3f43 bump plugin shared library core reference version 2022-10-13 13:53:28 -05:00
0ce9dec3ea fix issue with new remote command execution 2022-10-13 13:29:39 -05:00
069e6a0517 improve penalty colors 2022-10-13 13:29:39 -05:00
778feb8024 Fixed [JsonIgnore]
Fixed migration penalty creation
Fixed on migration command execution
Moved out CreatePenalty
Removed ClientId & AdminId since handled by Penalties
2022-10-13 13:29:39 -05:00
44f22dae3a update mute plugin to utilize new interaction forms
bump shared library core version
2022-10-13 13:29:39 -05:00
cf3209e1d0 Added !unmute, !tempmute, !listmutes
Quick fix for PowerShell IE use

Makes date readable for target player

Resolved translation string inconsistencies

Minor code cleanups

Initial commit from review

Cleaned up code & amended a few checks

Comment typo

Fix infinite unmuting

Removed unnecessary checks (Unmuting an already unmuted player will not trigger MuteStateMeta creation (if already doesn't exist))
Resolved !listmutes showing expired mutes

Committing before refactor

Refactor from review

Removed reference to AdditionalProperty

Fix check for meta state when unmuting

Continued request solves main problem

Handle potential failed command execution

Missed CommandExecuted onJoin

Fix another PS Reference to Invoke-WebRequest

Fixes review issues & Cleaned up code
Adds support for Intercepting Commands via Plugin (Credit: @RaidMax)

Comparing

Revert formatting changes

Removing MuteList for Penalty
Added Mute, TempMute & Unmute Penalty

Fixed reference in Mute.csproj & Removed ListMutesCommand.cs
2022-10-13 13:29:39 -05:00
a15da15d3e fix issue with vpn detection using new interaction 2022-10-13 10:47:25 -05:00
3b83729457 add level color coding to target on penalty list for issue #265 2022-10-13 10:41:51 -05:00
407ce2bc8f fix argument call to interactions 2022-10-13 10:26:22 -05:00
24d91f228b update interactions to allow building custom forms 2022-10-12 21:06:18 -05:00
53cbd11008 update shared library to fix data library issue 2022-10-12 12:14:43 -05:00
186db53bad update plugins to support command interception 2022-10-12 10:32:45 -05:00
40466f84c4 add command interceptor functionality 2022-10-11 16:18:56 -05:00
bdb5a1c5f8 Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-10-05 09:51:24 -05:00
5d9e2b3bf1 Game Interface ported to T5. (#254)
* Implement game interface for IW5 and T5
2022-10-05 09:49:00 -05:00
1cf99869f6 remove unneeded check for has permission 2022-09-24 10:22:05 -05:00
12da0f463b add client tag to default game interface data 2022-09-24 10:06:07 -05:00
e88071684d provide client tag in game interface meta 2022-09-21 13:04:15 -05:00
cd6097d133 default user permission for guest requests 2022-09-19 22:01:34 -05:00
d5cf4451a2 NoClip Fix - Removed NoClipOff - Toggle Hide (#263)
* Usage of Hide is now consistent with NoClip; toggleable
Removed obsolete !NoClipOff
2022-09-11 11:51:10 -05:00
1e1e8bbe7b fix issue with game interface meta/provide full example 2022-09-11 11:46:13 -05:00
dadd236069 upgrade nuget packages 2022-09-09 09:45:46 -05:00
2380f23dbe implement profile interaction registration through plugins (mute and vpn detection implementation) 2022-09-08 15:03:38 -05:00
3cffdfdd9d Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-09-07 09:16:58 -05:00
400c5d1f4d increase security on webfront cookie state/update events 2022-09-06 15:44:13 -05:00
ca35fbb19f iw4x integration - add delay before sending up persistent data 2022-08-31 16:17:02 -05:00
809cb0b7f4 account for trailing color code on long cod4x names 2022-08-27 21:25:42 -05:00
18f23fd07d Adding Mute for IW4x (#257)
* Adding Mute for IW4x
2022-08-26 12:09:33 -05:00
7526f86dab fix issues with game interface reconnecting after rcon connection lost 2022-08-26 12:07:43 -05:00
527ffbaced actual fix of setpassword from web console 2022-08-20 11:34:52 -05:00
6f086ac565 modularize the game integration files and better organize the anticheat folder structure 2022-08-20 10:57:03 -05:00
cf4dd6a868 fix issue with set password 2022-08-20 10:42:34 -05:00
3efafa24ff map the g_password dvar for T7 parser 2022-08-17 21:57:13 -05:00
fe919251fb add chat/chatteam event mapping for T7 2022-08-16 18:37:35 -05:00
a67f7f9351 don't display client banned on webfront if a linked ban has been revoked but they haven't reconnected yet 2022-07-25 11:54:55 -05:00
e99ca3c140 add more cases to "About" regex rule numbering scheme 2022-07-25 10:33:44 -05:00
ccedb01e8d improve help display and add supported games list 2022-07-25 10:21:08 -05:00
841bcf6156 tweak for T6 parser 2022-07-25 09:10:12 -05:00
b381af5fba fix dvar regex for T7 2022-07-24 13:29:40 -05:00
125 changed files with 11712 additions and 2279 deletions

View File

@ -21,17 +21,18 @@
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="Jint" Version="3.0.0-beta-2042" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>

View File

@ -46,6 +46,8 @@ namespace IW4MAdmin.Application
public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; }
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
@ -83,7 +85,7 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -113,9 +115,11 @@ namespace IW4MAdmin.Application
_changeHistoryService = changeHistoryService;
_appConfig = appConfig;
Plugins = plugins;
InteractionRegistration = interactionRegistration;
}
public IEnumerable<IPlugin> Plugins { get; }
public IInteractionRegistration InteractionRegistration { get; }
public async Task ExecuteEvent(GameEvent newEvent)
{
@ -225,14 +229,13 @@ namespace IW4MAdmin.Application
{
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
var timeout = TimeSpan.FromSeconds(60);
while (!_tokenSource.IsCancellationRequested)
while (!_tokenSource.IsCancellationRequested) // main shutdown requested
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
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))
.Where(ut => ut.Value.task.IsCompleted)
.Select(ut => ut.Key)
.ToList();
@ -248,36 +251,16 @@ namespace IW4MAdmin.Application
}
// 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 newTaskServers = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
{
var tokenSource = new CancellationTokenSource();
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{
try
{
if (runningUpdateTasks.ContainsKey(server.EndPoint))
{
await server.ProcessUpdatesAsync(_tokenSource.Token)
.WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
}
}
catch (Exception e)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(e, "Failed to update status");
}
}
finally
{
server.IsInitialized = true;
}
}, tokenSource.Token), tokenSource, DateTime.Now));
var firstTokenSource = new CancellationTokenSource();
firstTokenSource.CancelAfter(timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(firstTokenSource.Token, _tokenSource.Token);
runningUpdateTasks.Add(server.EndPoint, (ProcessUpdateHandler(server, linkedTokenSource.Token), linkedTokenSource, DateTime.Now));
}
try
{
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
@ -285,7 +268,7 @@ namespace IW4MAdmin.Application
// 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)))
foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
@ -294,6 +277,25 @@ namespace IW4MAdmin.Application
}
}
private async Task ProcessUpdateHandler(Server server, CancellationToken token)
{
try
{
await server.ProcessUpdatesAsync(token);
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex, "Failed to update status");
}
}
finally
{
server.IsInitialized = true;
}
}
public async Task Init()
{
IsRunning = true;
@ -613,6 +615,7 @@ namespace IW4MAdmin.Application
{
IsRestartRequested = true;
Stop().GetAwaiter().GetResult();
_tokenSource.Dispose();
_tokenSource = new CancellationTokenSource();
}

View File

@ -7,6 +7,6 @@ foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $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
}
}

View File

@ -158,14 +158,14 @@
{
"Game": "IW5",
"Gametypes": [
{
"Name": "tdm",
"Alias": "Team Deathmatch"
},
{
"Name": "dom",
"Alias": "Domination"
},
{
"Name": "conf",
"Alias": "Kill Confirmed"
},
{
"Name": "ctf",
"Alias": "Capture The Flag"
@ -175,37 +175,29 @@
"Alias": "Demolition"
},
{
"Name": "dz",
"Alias": "Drop Zone"
},
{
"Name": "ffa",
"Name": "dm",
"Alias": "Free For All"
},
{
"Name": "gg",
"Alias": "Gun Game"
"Name": "grnd",
"Alias": "Drop Zone"
},
{
"Name": "hq",
"Alias": "Headquarters"
"Name": "gun",
"Alias": "Gun Game"
},
{
"Name": "koth",
"Alias": "Headquarters"
},
{
"Name": "inf",
"Name": "infect",
"Alias": "Infected"
},
{
"Name": "jug",
"Name": "jugg",
"Alias": "Juggernaut"
},
{
"Name": "kc",
"Alias": "Kill Confirmed"
},
{
"Name": "oic",
"Alias": "One In The Chamber"
@ -223,8 +215,12 @@
"Alias": "Team Defender"
},
{
"Name": "tj",
"Name": "tjugg",
"Alias": "Team Juggernaut"
},
{
"Name": "war",
"Alias": "Team Deathmatch"
}
]
},
@ -984,7 +980,83 @@
{
"Alias": "Village",
"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"
}
]
},
{

View File

@ -29,6 +29,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
GameDirectory = "main",
LocalizeText = "\x15",
};
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
@ -105,6 +106,8 @@ namespace IW4MAdmin.Application.EventParsers
{
{"say", GameEvent.EventType.Say},
{"sayteam", GameEvent.EventType.Say},
{"chat", GameEvent.EventType.Say},
{"chatteam", GameEvent.EventType.Say},
{"K", GameEvent.EventType.Kill},
{"D", GameEvent.EventType.Damage},
{"J", GameEvent.EventType.PreConnect},
@ -183,7 +186,7 @@ namespace IW4MAdmin.Application.EventParsers
if (matchResult.Success)
{
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Replace("\x15", "")
.Replace(Configuration.LocalizeText, "")
.Trim();
if (message.Length > 0)

View File

@ -14,6 +14,7 @@ namespace IW4MAdmin.Application.EventParsers
{
public string GameDirectory { get; set; }
public ParserRegex Say { get; set; }
public string LocalizeText { get; set; }
public ParserRegex Join { get; set; }
public ParserRegex JoinTeam { get; set; }
public ParserRegex Quit { get; set; }

View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
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();
}
public static object GetId(this Server server)
{
return server.GetIdForServer().GetAwaiter().GetResult();
}
}

View File

@ -158,8 +158,6 @@ namespace IW4MAdmin
await E.Origin.Lock();
}
var canExecuteCommand = true;
try
{
if (!await ProcessEvent(E))
@ -188,32 +186,31 @@ namespace IW4MAdmin
}
}
try
var canExecuteCommand = Manager.CommandInterceptors.All(interceptor =>
{
var loginPlugin = Manager.Plugins.FirstOrDefault(plugin => plugin.Name == "Login");
if (loginPlugin != null)
try
{
await loginPlugin.OnEventAsync(E, this);
return interceptor(E);
}
catch
{
return true;
}
});
if (!canExecuteCommand)
{
E.Origin.Tell(_translationLookup["SERVER_COMMANDS_INTERCEPTED"]);
}
catch (AuthorizationException e)
else if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd)
{
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name, E.Origin.ToString());
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
E.Origin.ToString());
await cmd.ExecuteAsync(E);
}
var pluginTasks = Manager.Plugins
.Where(plugin => plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
@ -676,7 +673,7 @@ namespace IW4MAdmin
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
if (E.Extra == null)
@ -740,6 +737,8 @@ namespace IW4MAdmin
{
lastGameTime = E.GameTime.Value;
}
MatchStartTime = DateTime.Now;
}
else if (E.Type == GameEvent.EventType.MapEnd)
@ -750,6 +749,8 @@ namespace IW4MAdmin
{
lastGameTime = E.GameTime.Value;
}
MatchEndTime = DateTime.Now;
}
else if (E.Type == GameEvent.EventType.Tell)
@ -866,6 +867,12 @@ namespace IW4MAdmin
/// <returns></returns>
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 statusResponse = await this.GetStatusAsync(token);
@ -873,7 +880,7 @@ namespace IW4MAdmin
{
return null;
}
var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -1297,7 +1304,7 @@ namespace IW4MAdmin
this.MaxClients = maxplayers;
this.FSGame = game.Value;
this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
this.IP = ip.Value is "localhost" or "0.0.0.0" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
this.GamePassword = gamePassword.Value;
UpdateMap(mapname);

View File

@ -30,6 +30,7 @@ using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization;
using IW4MAdmin.Application.QueryHelpers;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
@ -38,6 +39,7 @@ using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
using WebfrontCore.QueryHelpers.Models;
namespace IW4MAdmin.Application
{
@ -431,6 +433,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>, ClientResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>()
@ -451,6 +454,8 @@ namespace IW4MAdmin.Application
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton<IRemoteCommandService, RemoteCommandService>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);

View File

@ -27,7 +27,8 @@ public class
await using var context = _contextFactory.CreateContext();
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
join client in context.Clients

View File

@ -22,7 +22,7 @@ public class GeoLocationService : IGeoLocationService
try
{
using var reader = new DatabaseReader(_sourceAddress);
reader.TryCountry(address, out country);
country = reader.Country(address);
}
catch
{

View File

@ -0,0 +1,160 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
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 Enumerable.Empty<IInteractionData>();
// fixme: multi-threading is broken when dealing with script plugins
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);
}
}
}
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();
}
}

View File

@ -0,0 +1,107 @@
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 (success, 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
};
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 (System.OperationCanceledException)
{
response = new[]
{
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
}
};
}
return (!remoteEvent.Failed, response);
}
}

View File

@ -10,9 +10,11 @@ using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IW4MAdmin.Application.Extensions;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
@ -53,7 +55,7 @@ namespace IW4MAdmin.Application.Misc
Watcher = new FileSystemWatcher
{
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}",
NotifyFilter = NotifyFilters.Size,
NotifyFilter = NotifyFilters.LastWrite,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
@ -110,14 +112,19 @@ namespace IW4MAdmin.Application.Misc
}
}
_scriptEngine?.Dispose();
_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(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly
typeof(CancellationTokenSource).Assembly,
typeof(Data.Models.Client.EFClient).Assembly,
typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));
@ -161,11 +168,19 @@ namespace IW4MAdmin.Application.Misc
}
}
async Task<bool> OnLoadTask()
{
await OnLoadAsync(manager);
return true;
}
var loadComplete = false;
try
{
if (pluginObject.isParser)
{
await OnLoadAsync(manager);
loadComplete = await OnLoadTask();
IsParser = true;
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject();
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject();
@ -178,30 +193,34 @@ namespace IW4MAdmin.Application.Misc
{
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
await configWrapper.InitializeAsync();
_scriptEngine.SetValue("_configHandler", configWrapper);
await OnLoadAsync(manager);
if (!loadComplete)
{
_scriptEngine.SetValue("_configHandler", configWrapper);
loadComplete = await OnLoadTask();
}
}
if (!firstRun)
if (!firstRun && !loadComplete)
{
await OnLoadAsync(manager);
loadComplete = await OnLoadTask();
}
_successfullyLoaded = true;
_successfullyLoaded = loadComplete;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location, ex.JavaScriptStackTrace);
throw new PluginException("An error occured while initializing 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}",
nameof(Initialize), _fileName, jsEx.Location);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace}",
nameof(Initialize), _fileName, jsEx.Location, jsEx.JavaScriptStackTrace);
throw new PluginException("An error occured while initializing script plugin");
}
@ -229,39 +248,71 @@ namespace IW4MAdmin.Application.Misc
return;
}
var shouldRelease = false;
try
{
await _onProcessing.WaitAsync();
_scriptEngine.SetValue("_gameEvent", gameEvent);
shouldRelease = true;
WrapJavaScriptErrorHandling(() =>
{
_scriptEngine.SetValue("_gameEvent", gameEvent);
_scriptEngine.SetValue("_server", server);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
return _scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}, new { EventType = gameEvent.Type }, server);
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
}
public Task OnLoadAsync(IManager manager)
{
_logger.LogDebug("OnLoad executing for {Name}", Name);
WrapJavaScriptErrorHandling(() =>
{
_scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", BeginSetDvar);
return _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
});
return Task.CompletedTask;
}
public Task OnTickAsync(Server server)
{
WrapJavaScriptErrorHandling(() =>
{
_scriptEngine.SetValue("_server", server);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
_scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}
return _scriptEngine.Evaluate("plugin.onTickAsync(_server)");
});
catch (JavaScriptException ex)
return Task.CompletedTask;
}
public async Task OnUnloadAsync()
{
if (!_successfullyLoaded)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex,
"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");
return;
}
catch (Exception ex)
try
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex,
"Encountered error while running {MethodName} for script plugin {Plugin} with event type {EventType}",
nameof(OnEventAsync), _fileName, gameEvent.Type);
}
await _onProcessing.WaitAsync();
throw new PluginException("An error occured while executing action for script plugin");
_logger.LogDebug("OnUnload executing for {Name}", Name);
WrapJavaScriptErrorHandling(() => _scriptEngine.Evaluate("plugin.onUnloadAsync()"));
}
finally
{
if (_onProcessing.CurrentCount == 0)
@ -271,71 +322,72 @@ namespace IW4MAdmin.Application.Misc
}
}
public Task OnLoadAsync(IManager manager)
public T ExecuteAction<T>(Delegate action, CancellationToken token, params object[] param)
{
var shouldRelease = false;
try
{
_logger.LogDebug("OnLoad executing for {Name}", Name);
_scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask;
using var forceTimeout = new CancellationTokenSource(5000);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
_onProcessing.Wait(combined.Token);
shouldRelease = true;
_logger.LogDebug("Executing action for {Name}", Name);
return WrapJavaScriptErrorHandling(T() =>
{
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
},
new
{
Params = string.Join(", ",
param?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
Enumerable.Empty<string>())
});
}
catch (JavaScriptException ex)
finally
{
_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");
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
}
public async Task OnTickAsync(Server server)
public T WrapDelegate<T>(Delegate act, CancellationToken token, params object[] args)
{
_scriptEngine.SetValue("_server", server);
await Task.FromResult(_scriptEngine.Evaluate("plugin.onTickAsync(_server)"));
}
public Task OnUnloadAsync()
{
if (!_successfullyLoaded)
{
return Task.CompletedTask;
}
var shouldRelease = false;
try
{
_scriptEngine.Evaluate("plugin.onUnloadAsync()");
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>())
});
}
catch (JavaScriptException ex)
finally
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("A runtime error occured while executing action for script plugin");
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
return Task.CompletedTask;
}
/// <summary>
@ -356,6 +408,12 @@ namespace IW4MAdmin.Application.Misc
string name = dynamicCommand.name;
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
if (dynamicCommand.permission is Data.Models.Client.EFClient.Permission perm)
{
dynamicCommand.permission = perm.ToString();
}
string permission = dynamicCommand.permission;
List<Server.Game> supportedGames = null;
var targetRequired = false;
@ -439,7 +497,6 @@ namespace IW4MAdmin.Application.Misc
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
@ -458,18 +515,15 @@ namespace IW4MAdmin.Application.Misc
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginGetDvar(dvarName, result =>
var operationTimeout = TimeSpan.FromSeconds(5);
void OnComplete(IAsyncResult result)
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
_onProcessing.Wait();
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
@ -479,31 +533,78 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success)
});
}
finally
catch (JavaScriptException ex)
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
using (LogContext.PushProperty("Server", server.ToString()))
{
_onProcessing.Release();
_logger.LogError(ex, "Could not invoke BeginGetDvar callback for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
}, tokenSource.Token);
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
new Thread(() =>
{
if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogDebug("Not getting DVar because match recently ended");
}
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = (false, (string)null)
});
}
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout);
server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).ContinueWith(action =>
{
if (action.IsCompletedSuccessfully)
{
OnComplete(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = (false, (string)null)
});
}
});
}).Start();
}
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginSetDvar(dvarName, dvarValue, result =>
var operationTimeout = TimeSpan.FromSeconds(5);
void OnComplete(IAsyncResult result)
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
_onProcessing.Wait();
var success = (bool)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
@ -513,14 +614,102 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success)
});
}
finally
catch (JavaScriptException ex)
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
using (LogContext.PushProperty("Server", server.ToString()))
{
_onProcessing.Release();
_logger.LogError(ex, "Could complete BeginSetDvar for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
}, tokenSource.Token);
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name);
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
new Thread(() =>
{
if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogDebug("Not setting DVar because match recently ended");
}
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = false
});
}
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout);
server.SetDvarAsync(dvarName, dvarValue, token: tokenSource.Token).ContinueWith(action =>
{
if (action.IsCompletedSuccessfully)
{
OnComplete(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = false
});
}
});
}).Start();
}
private T WrapJavaScriptErrorHandling<T>(Func<T> work, object additionalData = null, Server server = null,
[CallerMemberName] string methodName = "")
{
using (LogContext.PushProperty("Server", server?.ToString()))
{
try
{
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);
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);
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));
throw new PluginException("An error occured while executing action for script plugin");
}
}
}
}
@ -534,7 +723,6 @@ namespace IW4MAdmin.Application.Misc
return true;
}
result = JsValue.Null;
return false;
}

View File

@ -14,10 +14,13 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
private Action _actions;
private Delegate _jsAction;
private string _actionName;
private int _interval = DefaultInterval;
private long _waitingCount;
private const int DefaultDelay = 0;
private const int DefaultInterval = 1000;
private const int MaxWaiting = 10;
private readonly ILogger _logger;
private readonly ManualResetEventSlim _onRunningTick = new();
private readonly SemaphoreSlim _onRunningTick = new(1, 1);
private SemaphoreSlim _onDependentAction;
public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger)
@ -31,6 +34,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
Stop();
}
_onRunningTick.Dispose();
}
@ -50,13 +54,13 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
throw new ArgumentException("Timer interval must be at least 20ms");
}
Stop();
_logger.LogDebug("Starting script timer...");
_onRunningTick.Set();
_timer ??= new Timer(callback => _actions(), null, delay, interval);
_interval = interval;
IsRunning = true;
}
@ -76,7 +80,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
return;
}
_logger.LogDebug("Stopping script timer...");
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose();
@ -100,53 +104,90 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_jsAction = action;
_actionName = actionName;
_actions = OnTick;
_actions = OnTickInternal;
}
private void ReleaseThreads()
private void ReleaseThreads(bool releaseOnRunning, bool releaseOnDependent)
{
_onRunningTick.Set();
if (_onDependentAction?.CurrentCount != 0)
if (releaseOnRunning && _onRunningTick.CurrentCount == 0)
{
return;
_logger.LogDebug("-Releasing OnRunning for timer");
_onRunningTick.Release(1);
}
_onDependentAction?.Release(1);
if (releaseOnDependent && _onDependentAction?.CurrentCount == 0)
{
_onDependentAction?.Release(1);
}
}
private void OnTick()
private async void OnTickInternal()
{
var releaseOnRunning = false;
var releaseOnDependent = false;
try
{
if (!_onRunningTick.IsSet)
try
{
if (Interlocked.Read(ref _waitingCount) > MaxWaiting)
{
_logger.LogWarning("Reached max number of waiting count ({WaitingCount}) for {OnTick}",
_waitingCount, nameof(OnTickInternal));
return;
}
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(OnTick));
nameof(OnTickInternal));
return;
}
_onRunningTick.Reset();
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
_onDependentAction?.WaitAsync().Wait();
try
{
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
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;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);
ReleaseThreads();
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsex)
catch (Exception ex) when (ex.InnerException is JavaScriptException jsx)
{
_logger.LogError(jsex,
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName,
jsex.Location);
ReleaseThreads();
_logger.LogError(jsx,
"Could not execute timer tick for script action {ActionName} [{@LocationInfo}] [{@StackTrace}]",
_actionName,
jsx.Location, jsx.JavaScriptStackTrace);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
_onRunningTick.Set();
ReleaseThreads();
}
finally
{
ReleaseThreads(releaseOnRunning, releaseOnDependent);
Interlocked.Decrement(ref _waitingCount);
}
}

View File

@ -0,0 +1,290 @@
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.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.QueryHelpers.Models;
using EFClient = Data.Models.Client.EFClient;
namespace IW4MAdmin.Application.QueryHelpers;
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
{
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)
{
_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);
}
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)
{
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
{
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;
}
}

View File

@ -53,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
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.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -147,7 +147,7 @@ namespace IW4MAdmin.Application.RConParsers
{
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
{
if (action.Exception is null)
if (action.IsCompletedSuccessfully)
{
callback?.Invoke(new AsyncResult
{
@ -164,7 +164,7 @@ namespace IW4MAdmin.Application.RConParsers
AsyncState = (false, (string)null)
});
}
}, token);
}, CancellationToken.None);
}
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
@ -176,16 +176,16 @@ namespace IW4MAdmin.Application.RConParsers
return new StatusResponse
{
Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern)
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus),
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;
}
@ -193,10 +193,10 @@ namespace IW4MAdmin.Application.RConParsers
string value = null;
foreach (var line in response)
{
var regex = Regex.Match(line, groupPattern);
if (regex.Success)
var regex = Regex.Match(line, parserRegex.Pattern);
if (regex.Success && parserRegex.GroupMapping.ContainsKey(groupType))
{
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
}
}
@ -227,7 +227,7 @@ namespace IW4MAdmin.Application.RConParsers
{
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
{
if (action.Exception is null)
if (action.Exception is null && !action.IsCanceled)
{
callback?.Invoke(new AsyncResult
{
@ -244,7 +244,7 @@ namespace IW4MAdmin.Application.RConParsers
AsyncState = false
});
}
}, token);
}, CancellationToken.None);
}
private List<EFClient> ClientsFromStatus(string[] Status)

View File

@ -32,6 +32,7 @@ namespace Data.Context
public DbSet<EFClientMessage> ClientMessages { get; set; }
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
public DbSet<EFClientStatistics> ClientStatistics { get; set; }
public DbSet<EFHitLocation> HitLocations { get; set; }
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
public DbSet<EFWeapon> Weapons { get; set; }
@ -87,7 +88,8 @@ namespace Data.Context
// make network id unique
modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(e => e.NetworkId);
entity.HasIndex(client => client.NetworkId);
entity.HasIndex(client => client.LastConnection);
entity.HasAlternateKey(client => new
{
client.NetworkId,

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

View File

@ -96,6 +96,8 @@ namespace Data.Migrations.MySql
b.HasIndex("CurrentAliasId");
b.HasIndex("LastConnection");
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

View File

@ -103,6 +103,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("CurrentAliasId");
b.HasIndex("LastConnection");
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

View File

@ -94,6 +94,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("CurrentAliasId");
b.HasIndex("LastConnection");
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);

View File

@ -18,6 +18,9 @@ namespace Data.Models
Unban,
Any,
Unflag,
Mute,
TempMute,
Unmute,
Other = 100
}

View File

@ -9,7 +9,7 @@ foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}\Localization\IW4MAdmin.{1}.json" -f $PublishDir, $localization
$response = Invoke-WebRequest $url
$response = Invoke-WebRequest $url -UseBasicParsing
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
}
@ -20,4 +20,4 @@ Minor = $versionInfo.ProductMinorPart
Build = $versionInfo.ProductBuildPart
Revision = $versionInfo.ProductPrivatePart
}
$json | ConvertTo-Json | Out-File -FilePath ("{0}\VersionInformation.json" -f $PublishDir) -Encoding ASCII
$json | ConvertTo-Json | Out-File -FilePath ("{0}\VersionInformation.json" -f $PublishDir) -Encoding ASCII

View File

@ -39,7 +39,7 @@ else
Write-Output "Retrieving latest version info..."
$releaseInfo = (Invoke-WebRequest $releasesUri | ConvertFrom-Json) | Select -First 1
$releaseInfo = (Invoke-WebRequest $releasesUri -UseBasicParsing | ConvertFrom-Json) | Select -First 1
$asset = $releaseInfo.assets | Where-Object name -like $assetPattern | Select -First 1
$downloadUri = $asset.browser_download_url
$filename = Split-Path $downloadUri -leaf
@ -55,7 +55,7 @@ if (!$Silent)
Write-Output "Downloading update. This might take a moment..."
$fileDownload = Invoke-WebRequest -Uri $downloadUri
$fileDownload = Invoke-WebRequest -Uri $downloadUri -UseBasicParsing
if ($fileDownload.StatusDescription -ne "OK")
{
throw "Could not update IW4MAdmin. ($fileDownload.StatusDescription)"

View File

@ -52,13 +52,6 @@ waitForAttack()
}
}
getHttpString( url )
{
request = httpGet( url );
request waittill( "done", success, data );
request destroy();
}
runRadarUpdates()
{
interval = getDvarInt( "sv_printradar_updateinterval", 500 );

View File

@ -0,0 +1,23 @@
# IW5
This expands IW4M-Admins's Anti-cheat to Plutonium IW5
## Installation
Add ``_customcallbacks.gsc`` into the scripts folder. (%localappdata%\Plutonium\storage\iw5\scripts)
For more info check out Chase's [how-to guide](https://forum.plutonium.pw/topic/10738/tutorial-loading-custom-gsc-scripts).
You need to add this to you ``StatsPluginSettings.json`` found in your IW4M-Admin configuration folder.
```
"IW5": {
"Recoil": [
"iw5_1887_mp.*",
"turret_minigun_mp"
],
"Button": [
".*akimbo.*"
]
}
```
[Example](https://imgur.com/Ji9AafI)

View File

@ -24,12 +24,12 @@ Add this to the WeaponNameParserConfigurations List in the StatsPluginSettings.j
}
```
Now create the following entry for __EVERY__ T6 server you are using this on in the ServerDetectionTypes list:
Now update the `GameDetectionTypes` list with the following, if it does not already exist:
```
"1270014976": [
"T6": [
"Offset",
"Strain",
"Snap"
]
```
"Snap",
"Strain"
]
```

View File

@ -0,0 +1,715 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
Init()
{
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// setup default vars
level.eventBus = spawnstruct();
level.eventBus.inVar = "sv_iw4madmin_in";
level.eventBus.outVar = "sv_iw4madmin_out";
level.eventBus.failKey = "fail";
level.eventBus.timeoutKey = "timeout";
level.eventBus.timeout = 30;
level.commonFunctions = spawnstruct();
level.commonFunctions.setDvar = "SetDvarIfUninitialized";
level.commonKeys = spawnstruct();
level.notifyTypes = spawnstruct();
level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized";
level.notifyTypes.integrationBootstrapInitialized = "IntegrationBootstrapInitialized";
level.clientDataKey = "clientData";
level.eventTypes = spawnstruct();
level.eventTypes.localClientEvent = "client_event";
level.eventTypes.clientDataReceived = "ClientDataReceived";
level.eventTypes.clientDataRequested = "ClientDataRequested";
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
level.iw4madminIntegrationDebug = 0;
// map the event type to the handler
level.eventCallbacks = [];
level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived;
level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand;
level.eventCallbacks[level.eventTypes.setClientDataCompleted] = ::OnSetClientDataCompleted;
level.clientCommandCallbacks = [];
level.clientCommandRusAsTarget = [];
level.logger = spawnstruct();
level.overrideMethods = [];
level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
InitializeLogger();
wait ( 0.05 ); // needed to give script engine time to propagate notifies
level notify( level.notifyTypes.integrationBootstrapInitialized );
level waittill( level.notifyTypes.gameFunctionsInitialized );
LogDebug( "Integration received notify that game functions are initialized" );
_SetDvarIfUninitialized( level.eventBus.inVar, "" );
_SetDvarIfUninitialized( level.eventBus.outVar, "" );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
// start long running tasks
level thread MonitorClientEvents();
level thread MonitorBus();
level thread OnPlayerConnect();
}
//////////////////////////////////
// Client Methods
//////////////////////////////////
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( _IsBot( player ) )
{
// we don't want to track bots
continue;
}
if ( !IsDefined( player.pers[level.clientDataKey] ) )
{
player.pers[level.clientDataKey] = spawnstruct();
}
player thread OnPlayerSpawned();
player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval();
}
}
OnPlayerSpawned()
{
self endon( "disconnect" );
for ( ;; )
{
self waittill( "spawned_player" );
self PlayerSpawnEvents();
}
}
OnPlayerJoinedTeam()
{
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_team" );
// join spec and join team occur at the same moment - out of order logging would be problematic
wait( 0.25 );
LogPrint( GenerateJoinTeamString( false ) );
}
}
OnPlayerJoinedSpectators()
{
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_spectators" );
LogPrint( GenerateJoinTeamString( true ) );
}
}
OnGameEnded()
{
for ( ;; )
{
level waittill( "game_ended" );
// note: you can run data code here but it's possible for
// data to get truncated, so we will try a timer based approach for now
}
}
DisplayWelcomeData()
{
self endon( "disconnect" );
clientData = self.pers[level.clientDataKey];
if ( clientData.permissionLevel == "User" || clientData.permissionLevel == "Flagged" )
{
return;
}
self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel );
wait( 2.0 );
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
PlayerSpawnEvents()
{
self endon( "disconnect" );
clientData = self.pers[level.clientDataKey];
// this gives IW4MAdmin some time to register the player before making the request;
// although probably not necessary some users might have a slow database or poll rate
wait ( 2 );
if ( IsDefined( clientData.state ) && clientData.state == "complete" )
{
return;
}
self RequestClientBasicData();
}
PlayerTrackingOnInterval()
{
self endon( "disconnect" );
for ( ;; )
{
wait ( 120 );
if ( IsAlive( self ) )
{
self SaveTrackingMetrics();
}
}
}
MonitorClientEvents()
{
level endon( "game_ended" );
for ( ;; )
{
level waittill( level.eventTypes.localClientEvent, client );
LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
eventHandler = level.eventCallbacks[client.event.type];
if ( IsDefined( eventHandler ) )
{
client [[eventHandler]]( client.event );
LogDebug( "notify client for " + client.event.type );
client notify( level.eventTypes.localClientEvent, client.event );
}
client.eventData = [];
}
}
//////////////////////////////////
// Helper Methods
//////////////////////////////////
_IsBot( entity )
{
// there already is a cgame function exists as "IsBot", for IW4, but unsure what all titles have it defined,
// so we are defining it here
return IsDefined( entity.pers["isBot"] ) && entity.pers["isBot"];
}
_SetDvarIfUninitialized( dvarName, dvarValue )
{
[[level.overrideMethods[level.commonFunctions.setDvar]]]( dvarName, dvarValue );
}
NotImplementedFunction( a, b, c, d, e, f )
{
LogWarning( "Function not implemented" );
}
// Not every game can output to console or even game log.
// Adds a very basic logging system that every
// game specific script can extend.accumulate
// Logging to dvars used as example.
InitializeLogger()
{
level.logger._logger = [];
RegisterLogger( ::Log2Dvar );
RegisterLogger( ::Log2IngamePrint );
level.logger.debug = ::LogDebug;
level.logger.error = ::LogError;
level.logger.warning = ::LogWarning;
}
_Log( LogLevel, message )
{
for( i = 0; i < level.logger._logger.size; i++ )
{
[[level.logger._logger[i]]]( LogLevel, message );
}
}
LogDebug( message )
{
if ( level.iw4madminIntegrationDebug )
{
_Log( "debug", level.eventBus.gamename + ": " + message );
}
}
LogError( message )
{
_Log( "error", message );
}
LogWarning( message )
{
_Log( "warning", message );
}
Log2Dvar( LogLevel, message )
{
switch ( LogLevel )
{
case "debug":
SetDvar( "sv_iw4madmin_last_debug", message );
break;
case "error":
SetDvar( "sv_iw4madmin_last_error", message );
break;
case "warning":
SetDvar( "sv_iw4madmin_last_warning", message );
break;
}
}
Log2IngamePrint( LogLevel, message )
{
switch ( LogLevel )
{
case "debug":
IPrintLn( "[DEBUG] " + message );
break;
case "error":
IPrintLn( "[ERROR] " + message );
break;
case "warning":
IPrintLn( "[WARN] " + message );
break;
}
}
RegisterLogger( logger )
{
level.logger._logger[level.logger._logger.size] = logger;
}
RequestClientMeta( metaKey )
{
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
level thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
}
RequestClientBasicData()
{
getClientDataEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "None", self, "" );
level thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
}
IncrementClientMeta( metaKey, incrementValue, clientId )
{
SetClientMeta( metaKey, incrementValue, clientId, "increment" );
}
DecrementClientMeta( metaKey, decrementValue, clientId )
{
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
if ( IsDefined( self.joining_team ) )
{
team = self.joining_team;
}
else
{
if ( isSpectator || !IsDefined( team ) )
{
team = "spectator";
}
}
guid = self GetXuid();
if ( guid == "0" )
{
guid = self.guid;
}
if ( !IsDefined( guid ) || guid == "0" )
{
guid = "undefined";
}
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
}
SetClientMeta( metaKey, metaValue, clientId, direction )
{
data = "key=" + metaKey + "|value=" + metaValue;
clientNumber = -1;
if ( IsDefined ( clientId ) )
{
data = data + "|clientId=" + clientId;
clientNumber = -1;
}
if ( IsDefined( direction ) )
{
data = data + "|direction=" + direction;
}
if ( IsPlayer( self ) )
{
clientNumber = self getEntityNumber();
}
setClientMetaEvent = BuildEventRequest( true, level.eventTypes.setClientDataRequested, "Meta", clientNumber, data );
level thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
}
SaveTrackingMetrics()
{
if ( !IsDefined( self.persistentClientId ) )
{
return;
}
LogDebug( "Saving tracking metrics for " + self.persistentClientId );
if ( !IsDefined( self.lastShotCount ) )
{
self.lastShotCount = 0;
}
currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
change = currentShotCount - self.lastShotCount;
self.lastShotCount = currentShotCount;
LogDebug( "Total Shots Fired increased by " + change );
if ( !IsDefined( change ) )
{
change = 0;
}
if ( change == 0 )
{
return;
}
IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
}
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
{
if ( !IsDefined( data ) )
{
data = "";
}
if ( !IsDefined( eventSubtype ) )
{
eventSubtype = "None";
}
if ( IsPlayer( entOrId ) )
{
entOrId = entOrId getEntityNumber();
}
request = "0";
if ( responseExpected )
{
request = "1";
}
request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data;
return request;
}
MonitorBus()
{
level endon( "game_ended" );
for( ;; )
{
wait ( 0.1 );
// check to see if IW4MAdmin is ready to receive more data
if ( getDvar( level.eventBus.inVar ) == "" )
{
level notify( "bus_ready" );
}
eventString = getDvar( level.eventBus.outVar );
if ( eventString == "" )
{
continue;
}
LogDebug( "-> " + eventString );
NotifyClientEvent( strtok( eventString, ";" ) );
SetDvar( level.eventBus.outVar, "" );
}
}
QueueEvent( request, eventType, notifyEntity )
{
level endon( "game_ended" );
start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds
timedOut = "";
while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait )
{
level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 );
if ( GetDvar( level.eventBus.inVar ) != "" )
{
LogDebug( "A request is already in progress..." );
timedOut = "set";
continue;
}
timedOut = "unset";
}
if ( timedOut == "set")
{
LogDebug( "Timed out waiting for response..." );
if ( IsDefined( notifyEntity ) )
{
notifyEntity NotifyClientEventTimeout( eventType );
}
SetDvar( level.eventBus.inVar, "" );
return;
}
LogDebug("<- " + request );
SetDvar( level.eventBus.inVar, request );
}
ParseDataString( data )
{
if ( !IsDefined( data ) )
{
LogDebug( "No data to parse" );
return [];
}
dataParts = strtok( data, "|" );
dict = [];
for ( i = 0; i < dataParts.size; i++ )
{
part = dataParts[i];
splitPart = strtok( part, "=" );
key = splitPart[0];
value = splitPart[1];
dict[key] = value;
dict[i] = key;
}
return dict;
}
NotifyClientEventTimeout( eventType )
{
// todo: make this actual eventing
if ( eventType == level.eventTypes.clientDataRequested )
{
self.pers["clientData"].state = level.eventBus.timeoutKey;
}
}
NotifyClientEvent( eventInfo )
{
origin = getPlayerFromClientNum( int( eventInfo[3] ) );
target = getPlayerFromClientNum( int( eventInfo[4] ) );
event = spawnstruct();
event.type = eventInfo[1];
event.subtype = eventInfo[2];
event.data = eventInfo[5];
event.origin = origin;
event.target = target;
if ( IsDefined( event.data ) )
{
LogDebug( "NotifyClientEvent->" + event.data );
}
if ( int( eventInfo[3] ) != -1 && !IsDefined( origin ) )
{
LogDebug( "origin is null but the slot id is " + int( eventInfo[3] ) );
}
if ( int( eventInfo[4] ) != -1 && !IsDefined( target ) )
{
LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) );
}
if ( IsDefined( target ) )
{
client = event.target;
}
else if ( IsDefined( origin ) )
{
client = event.origin;
}
else
{
LogDebug( "Neither origin or target are set but we are a Client Event, aborting" );
return;
}
client.event = event;
level notify( level.eventTypes.localClientEvent, client );
}
GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
for ( i = 0; i < level.players.size; i++ )
{
if ( level.players[i] getEntityNumber() == clientNum )
{
return level.players[i];
}
}
return undefined;
}
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
{
if ( IsDefined( level.clientCommandCallbacks[commandName] ) && IsDefined( shouldOverwrite ) && !shouldOverwrite )
{
return;
}
level.clientCommandCallbacks[commandName] = callback;
level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean
}
//////////////////////////////////
// Event Handlers
/////////////////////////////////
OnClientDataReceived( event )
{
event.data = ParseDataString( event.data );
clientData = self.pers[level.clientDataKey];
if ( event.subtype == "Fail" )
{
LogDebug( "Received fail response" );
clientData.state = level.eventBus.failKey;
return;
}
if ( event.subtype == "Meta" )
{
if ( !IsDefined( clientData.meta ) )
{
clientData.meta = [];
}
metaKey = event.data[0];
clientData.meta[metaKey] = event.data[metaKey];
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
return;
}
clientData.permissionLevel = event.data["level"];
clientData.clientId = event.data["clientId"];
clientData.lastConnection = event.data["lastConnection"];
clientData.tag = event.data["tag"];
clientData.performance = event.data["performance"];
clientData.state = "complete";
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
}
OnExecuteCommand( event )
{
data = ParseDataString( event.data );
response = "";
command = level.clientCommandCallbacks[event.subtype];
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
executionContextEntity = event.origin;
if ( runAsTarget )
{
executionContextEntity = event.target;
}
if ( IsDefined( command ) )
{
response = executionContextEntity [[command]]( event, data );
}
else
{
LogDebug( "Unknown Client command->" + event.subtype );
}
// send back the response to the origin, but only if they're not the target
if ( IsDefined( response ) && response != "" && IsPlayer( event.origin ) && event.origin != event.target )
{
event.origin IPrintLnBold( response );
}
}
OnSetClientDataCompleted( event )
{
// IW4MAdmin let us know it persisted (success or fail)
LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
}

View File

@ -0,0 +1,590 @@
#include common_scripts\iw4x_utility;
Init()
{
level.eventBus.gamename = "IW4";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
level.overrideMethods[level.commonFunctions.changeTeam] = ::ChangeTeam;
level.overrideMethods[level.commonFunctions.getTeamCounts] = ::CountPlayers;
level.overrideMethods[level.commonFunctions.getMaxClients] = ::GetMaxClients;
level.overrideMethods[level.commonFunctions.getTeamBased] = ::GetTeamBased;
level.overrideMethods[level.commonFunctions.getClientTeam] = ::GetClientTeam;
level.overrideMethods[level.commonFunctions.getClientKillStreak] = ::GetClientKillStreak;
level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = ::BackupRestoreClientKillStreakData;
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetMaxClients()
{
return level.maxClients;
}
GetTeamBased()
{
return level.teamBased;
}
CountPlayers()
{
return maps\mp\gametypes\_teams::CountPlayers();
}
GetClientTeam()
{
if ( IsDefined( self.pers["team"] ) && self.pers["team"] == "allies" )
{
return "allies";
}
else if ( IsDefined( self.pers["team"] ) && self.pers["team"] == "axis" )
{
return "axis";
}
else
{
return "none";
}
}
GetClientKillStreak()
{
return int( self.pers["cur_kill_streak"] );
}
BackupRestoreClientKillStreakData( restore )
{
if ( restore )
{
foreach ( index, streakStruct in self.pers["killstreaks_backup"] )
{
self.pers["killstreaks"][index] = self.pers["killstreaks_backup"][index];
}
}
else
{
self.pers["killstreaks_backup"] = [];
foreach ( index, streakStruct in self.pers["killstreaks"] )
{
self.pers["killstreaks_backup"][index] = self.pers["killstreaks"][index];
}
}
}
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
{
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
}
ChangeTeam( team )
{
switch ( team )
{
case "allies":
self [[level.allies]]();
break;
case "axis":
self [[level.axis]]();
break;
case "spectator":
self [[level.spectator]]();
break;
}
}
GetTotalShotsFired()
{
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
Log2Console( logLevel, message )
{
PrintConsole( "[" + logLevel + "] " + message + "\n" );
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
SetPersistentData()
{
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
SplitGuid()
{
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl()
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Show();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
}
HideImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
if ( level.eventBus.gamename == "IW4" )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl()
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl()
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -0,0 +1,501 @@
#include common_scripts\utility;
Init()
{
level.eventBus.gamename = "IW5";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
{
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
SetPersistentData()
{
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
SplitGuid()
{
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl()
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self God();
self Noclip();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self God();
self Noclip();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );
}
HideImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self God();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self God();
self Show();
SetDvar( "sv_cheats", 0 );
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
if ( level.eventBus.gamename == "IW5" ) {
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl()
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl()
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -0,0 +1,451 @@
Init()
{
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
level.commonFunctions.changeTeam = "ChangeTeam";
level.commonFunctions.getTeamCounts = "GetTeamCounts";
level.commonFunctions.getMaxClients = "GetMaxClients";
level.commonFunctions.getTeamBased = "GetTeamBased";
level.commonFunctions.getClientTeam = "GetClientTeam";
level.commonFunctions.getClientKillStreak = "GetClientKillStreak";
level.commonFunctions.backupRestoreClientKillStreakData = "BackupRestoreClientKillStreakData";
level.commonFunctions.waitTillAnyTimeout = "WaitTillAnyTimeout";
level.overrideMethods[level.commonFunctions.changeTeam] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getTeamCounts] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getTeamBased] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getMaxClients] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getClientTeam] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getClientKillStreak] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = scripts\_integration_base::NotImplementedFunction;
// these can be overridden per game if needed
level.commonKeys.team1 = "allies";
level.commonKeys.team2 = "axis";
level.commonKeys.teamSpectator = "spectator";
level.eventTypes.connect = "connected";
level.eventTypes.disconnect = "disconnect";
level.eventTypes.joinTeam = "joined_team";
level.eventTypes.spawned = "spawned_player";
level.eventTypes.gameEnd = "game_ended";
level.iw4madminIntegrationDefaultPerformance = 200;
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon( level.eventTypes.gameEnd );
for ( ;; )
{
level waittill( level.eventTypes.connect, player );
if ( ![[level.overrideMethods[level.commonFunctions.getTeamBased]]]() )
{
continue;
}
teamToJoin = player GetTeamToJoin();
player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin );
player thread OnClientFirstSpawn();
player thread OnClientJoinedTeam();
player thread OnClientDisconnect();
}
}
OnClientDisconnect()
{
level endon( level.eventTypes.gameEnd );
self endon( "disconnect_logic_end" );
for ( ;; )
{
self waittill( level.eventTypes.disconnect );
scripts\_integration_base::LogDebug( "client is disconnecting" );
OnTeamSizeChanged();
self notify( "disconnect_logic_end" );
}
}
OnClientJoinedTeam()
{
self endon( level.eventTypes.disconnect );
for( ;; )
{
self waittill( level.eventTypes.joinTeam );
if ( IsDefined( self.wasAutoBalanced ) && self.wasAutoBalanced )
{
self.wasAutoBalanced = false;
continue;
}
newTeam = self [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
scripts\_integration_base::LogDebug( self.name + " switched to " + newTeam );
if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 )
{
OnTeamSizeChanged();
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
continue;
}
properTeam = self GetTeamToJoin();
if ( newTeam != properTeam )
{
self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( properTeam );
wait ( 0.1 );
self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
}
}
}
OnClientFirstSpawn()
{
self endon( level.eventTypes.disconnect );
timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.spawned );
if ( timeoutResult != "timeout" )
{
return;
}
scripts\_integration_base::LogDebug( "moving " + self.name + " to spectator because they did not spawn within expected duration" );
self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( level.commonKeys.teamSpectator );
}
OnTeamSizeChanged()
{
if ( level.players.size < 3 )
{
scripts\_integration_base::LogDebug( "not enough clients to autobalance" );
return;
}
if ( !IsDefined( GetSmallerTeam( 1 ) ) )
{
scripts\_integration_base::LogDebug( "teams are not unbalanced enough to auto balance" );
return;
}
toSwap = FindClientToSwap();
curentTeam = toSwap [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
otherTeam = level.commonKeys.team1;
if ( curentTeam == otherTeam )
{
otherTeam = level.commonKeys.team2;
}
toSwap.wasAutoBalanced = true;
if ( !IsDefined( toSwap.autoBalanceCount ) )
{
toSwap.autoBalanceCount = 1;
}
else
{
toSwap.autoBalanceCount++;
}
toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
scripts\_integration_base::LogDebug( "swapping " + toSwap.name + " from " + curentTeam + " to " + otherTeam );
toSwap [[level.overrideMethods[level.commonFunctions.changeTeam]]]( otherTeam );
wait ( 0.1 ); // give the killstreak on team switch clear event time to execute
toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
}
FindClientToSwap()
{
smallerTeam = GetSmallerTeam( 1 );
teamPool = level.commonKeys.team1;
if ( IsDefined( smallerTeam ) )
{
if ( smallerTeam == teamPool )
{
teamPool = level.commonKeys.team2;
}
}
else
{
teamPerformances = GetTeamPerformances();
team1Perf = teamPerformances[level.commonKeys.team1];
team2Perf = teamPerformances[level.commonKeys.team2];
teamPool = level.commonKeys.team1;
if ( team2Perf > team1Perf )
{
teamPool = level.commonKeys.team2;
}
}
client = GetBestSwapCandidate( teamPool );
if ( !IsDefined( client ) )
{
scripts\_integration_base::LogDebug( "could not find candidate to swap teams" );
}
else
{
scripts\_integration_base::LogDebug( "best candidate to swap teams is " + client.name );
}
return client;
}
GetBestSwapCandidate( team )
{
candidates = [];
maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();
for ( i = 0; i < maxClients; i++ )
{
candidates[i] = GetClosestPerformanceClientForTeam( team, candidates );
}
candidate = undefined;
foundCandidate = false;
for ( i = 0; i < maxClients; i++ )
{
if ( !IsDefined( candidates[i] ) )
{
continue;
}
candidate = candidates[i];
candidateKillStreak = candidate [[level.overrideMethods[level.commonFunctions.getClientKillStreak]]]();
scripts\_integration_base::LogDebug( "candidate killstreak is " + candidateKillStreak );
if ( candidateKillStreak > 3 )
{
scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " because their kill streak is too high" );
continue;
}
if ( IsDefined( candidate.autoBalanceCount ) && candidate.autoBalanceCount > 2 )
{
scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " they have been swapped too many times" );
continue;
}
foundCandidate = true;
break;
}
if ( foundCandidate )
{
return candidate;
}
return undefined;
}
GetClosestPerformanceClientForTeam( sourceTeam, excluded )
{
if ( !IsDefined( excluded ) )
{
excluded = [];
}
otherTeam = level.commonKeys.team1;
if ( sourceTeam == otherTeam )
{
otherTeam = level.commonKeys.team2;
}
teamPerformances = GetTeamPerformances();
players = level.players;
choice = undefined;
closest = 9999999;
for ( i = 0; i < players.size; i++ )
{
isExcluded = false;
for ( j = 0; j < excluded.size; j++ )
{
if ( excluded[j] == players[i] )
{
isExcluded = true;
break;
}
}
if ( isExcluded )
{
continue;
}
if ( players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]() != sourceTeam )
{
continue;
}
clientPerformance = players[i] GetClientPerformanceOrDefault();
sourceTeamNewPerformance = teamPerformances[sourceTeam] - clientPerformance;
otherTeamNewPerformance = teamPerformances[otherTeam] + clientPerformance;
candidateValue = Abs( sourceTeamNewPerformance - otherTeamNewPerformance );
scripts\_integration_base::LogDebug( "perf=" + clientPerformance + " candidateValue=" + candidateValue + " src=" + sourceTeamNewPerformance + " dst=" + otherTeamNewPerformance );
if ( !IsDefined( choice ) )
{
choice = players[i];
closest = candidateValue;
}
else if ( candidateValue < closest )
{
scripts\_integration_base::LogDebug( candidateValue + " is the new best value ");
choice = players[i];
closest = candidateValue;
}
}
scripts\_integration_base::LogDebug( choice.name + " is the best candidate to swap" + " with closest=" + closest );
return choice;
}
GetTeamToJoin()
{
smallerTeam = GetSmallerTeam( 1 );
if ( IsDefined( smallerTeam ) )
{
return smallerTeam;
}
teamPerformances = GetTeamPerformances( self );
if ( teamPerformances[level.commonKeys.team1] < teamPerformances[level.commonKeys.team2] )
{
scripts\_integration_base::LogDebug( "Team1 performance is lower, so selecting Team1" );
return level.commonKeys.team1;
}
else
{
scripts\_integration_base::LogDebug( "Team2 performance is lower, so selecting Team2" );
return level.commonKeys.team2;
}
}
GetSmallerTeam( minDiff )
{
teamCounts = [[level.overrideMethods[level.commonFunctions.getTeamCounts]]]();
team1Count = teamCounts[level.commonKeys.team1];
team2Count = teamCounts[level.commonKeys.team2];
maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();
if ( team1Count == team2Count )
{
return undefined;
}
if ( team2Count == maxClients / 2 )
{
scripts\_integration_base::LogDebug( "Team2 is full, so selecting Team1" );
return level.commonKeys.team1;
}
if ( team1Count == maxClients / 2 )
{
scripts\_integration_base::LogDebug( "Team1 is full, so selecting Team2" );
return level.commonKeys.team2;
}
sizeDiscrepancy = Abs( team1Count - team2Count );
if ( sizeDiscrepancy > minDiff )
{
scripts\_integration_base::LogDebug( "Team size differs by more than 1" );
if ( team1Count < team2Count )
{
scripts\_integration_base::LogDebug( "Team1 is smaller, so selecting Team1" );
return level.commonKeys.team1;
}
else
{
scripts\_integration_base::LogDebug( "Team2 is smaller, so selecting Team2" );
return level.commonKeys.team2;
}
}
return undefined;
}
GetTeamPerformances( ignoredClient )
{
players = level.players;
team1 = 0;
team2 = 0;
for ( i = 0; i < players.size; i++ )
{
if ( IsDefined( ignoredClient ) && players[i] == ignoredClient )
{
continue;
}
performance = players[i] GetClientPerformanceOrDefault();
clientTeam = players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
if ( clientTeam == level.commonKeys.team1 )
{
team1 = team1 + performance;
}
else
{
team2 = team2 + performance;
}
}
result = [];
result[level.commonKeys.team1] = team1;
result[level.commonKeys.team2] = team2;
return result;
}
GetClientPerformanceOrDefault()
{
clientData = self.pers[level.clientDataKey];
performance = level.iw4madminIntegrationDefaultPerformance;
if ( IsDefined( clientData ) && IsDefined( clientData.performance ) )
{
performance = int( clientData.performance );
}
return performance;
}

View File

@ -0,0 +1,522 @@
#include common_scripts\utility;
Init()
{
level.eventBus.gamename = "T5";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
//player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
{
return maps\mp\gametypes\_persistence::statGet( "total_shots" );
}
_SetDvarIfUninitialized(dvar, value)
{
maps\mp\_utility::set_dvar_if_unset(dvar, value);
}
_waittill_notify_or_timeout( msg, timer )
{
self endon( msg );
wait( timer );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
}
if (!self.godmode )
{
self enableInvulnerability();
self.godmode = true;
}
else
{
self.godmode = false;
self disableInvulnerability();
}
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
/*SetPersistentData()
{
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
SplitGuid()
{
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}*/
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl( event, data )
{
/*if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );*/
scripts\mp\_integration_base::LogWarning( "NoClip is not supported on T5!" );
}
HideImpl( event, data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 );
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl( event, data )
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

File diff suppressed because it is too large Load Diff

14
GameFiles/deploy.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
ECHO "Pluto IW5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
ECHO "Pluto T5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
xcopy /y .\GameInterface\_integration_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
ECHO "Pluto T6"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"

View File

@ -6,14 +6,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml
DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1
README.md = README.md
version.txt = version.txt
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
GameFiles\_integration.gsc = GameFiles\_integration.gsc
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"
@ -71,6 +69,39 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Cod", "Integra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Source", "Integrations\Source\Integrations.Source.csproj", "{9512295B-3045-40E0-9B7E-2409F2173E9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mute", "Plugins\Mute\Mute.csproj", "{259824F3-D860-4233-91D6-FF73D4DD8B18}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameFiles", "GameFiles", "{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterface", "{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55}"
ProjectSection(SolutionItems) = preProject
GameFiles\GameInterface\_integration_base.gsc = GameFiles\GameInterface\_integration_base.gsc
GameFiles\GameInterface\_integration_iw4x.gsc = GameFiles\GameInterface\_integration_iw4x.gsc
GameFiles\GameInterface\_integration_iw5.gsc = GameFiles\GameInterface\_integration_iw5.gsc
GameFiles\GameInterface\_integration_shared.gsc = GameFiles\GameInterface\_integration_shared.gsc
GameFiles\GameInterface\_integration_t5.gsc = GameFiles\GameInterface\_integration_t5.gsc
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IW4x", "IW4x", "{3EA564BD-3AC6-479B-96B6-CB059DCD0C77}"
ProjectSection(SolutionItems) = preProject
GameFiles\AntiCheat\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\AntiCheat\IW4x\userraw\scripts\_customcallbacks.gsc
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto T6", "Pluto T6", "{866F453D-BC89-457F-8B55-485494759B31}"
ProjectSection(SolutionItems) = preProject
GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc = GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc
GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src = GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src
GameFiles\AntiCheat\PT6\README.MD = GameFiles\AntiCheat\PT6\README.MD
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto IW5", "Pluto IW5", "{603725A4-BC0B-423B-955B-762C89E1C4C2}"
ProjectSection(SolutionItems) = preProject
GameFiles\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc = GameFiles\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc
GameFiles\AntiCheat\IW5\README.MD = GameFiles\AntiCheat\IW5\README.MD
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -375,6 +406,30 @@ Global
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x64.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x64.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x86.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x86.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x64.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x86.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Any CPU.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x64.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x64.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -389,6 +444,13 @@ Global
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F}
{A9348433-58C1-4B9C-8BB7-088B02529D9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B}
{9512295B-3045-40E0-9B7E-2409F2173E9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B}
{259824F3-D860-4233-91D6-FF73D4DD8B18} = {26E8B310-269E-46D4-A612-24601F16065F}
{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9} = {8C8F3945-0AEF-4949-A1F7-B18E952E50BC}
{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55} = {6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}
{AB83BAC0-C539-424A-BF00-78487C10753C} = {6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}
{3EA564BD-3AC6-479B-96B6-CB059DCD0C77} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{866F453D-BC89-457F-8B55-485494759B31} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{603725A4-BC0B-423B-955B-762C89E1C4C2} = {AB83BAC0-C539-424A-BF00-78487C10753C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -56,6 +56,11 @@ namespace Integrations.Cod
{
return await SendQueryAsyncInternal(type, parameters, token);
}
catch (RConException ex) when (ex.IsOperationCancelled)
{
_log.LogDebug(ex, "Could not complete RCon request");
throw;
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
@ -67,10 +72,7 @@ namespace Integrations.Cod
}
finally
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogDebug("Releasing OnComplete {Count}", ActiveQueries[Endpoint].OnComplete.CurrentCount);
}
_log.LogDebug("Releasing OnComplete {Count}", ActiveQueries[Endpoint].OnComplete.CurrentCount);
if (ActiveQueries[Endpoint].OnComplete.CurrentCount == 0)
{
@ -89,7 +91,11 @@ namespace Integrations.Cod
if (!ActiveQueries.TryGetValue(Endpoint, out var connectionState))
{
_log.LogError("Could not retrieve connection state");
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogError("Could not retrieve connection state");
}
throw new InvalidOperationException("Could not get connection state");
}
@ -104,7 +110,7 @@ namespace Integrations.Cod
{
_log.LogDebug("OnComplete did not complete before timeout {Count}",
connectionState.OnComplete.CurrentCount);
throw new RConException("Timed out waiting for access to rcon socket");
throw new RConException("Timed out waiting for access to rcon socket", true);
}
var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
@ -121,7 +127,7 @@ namespace Integrations.Cod
{
_log.LogDebug("Waiting for flood protect did not complete before timeout timeout {Count}",
connectionState.OnComplete.CurrentCount);
throw new RConException("Timed out waiting for flood protect to expire");
throw new RConException("Timed out waiting for flood protect to expire", true);
}
}
@ -145,7 +151,7 @@ namespace Integrations.Cod
switch (type)
{
case StaticHelpers.QueryType.GET_DVAR:
waitForResponse |= true;
waitForResponse = true;
payload = string
.Format(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
@ -161,15 +167,15 @@ namespace Integrations.Cod
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_STATUS:
waitForResponse |= true;
waitForResponse = true;
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_INFO:
waitForResponse |= true;
waitForResponse = true;
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND_STATUS:
waitForResponse |= true;
waitForResponse = true;
payload = string.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0")
.Select(Convert.ToByte).ToArray();
break;
@ -189,20 +195,9 @@ namespace Integrations.Cod
throw new RConException("Invalid character encountered when converting encodings");
}
byte[][] response = null;
byte[][] response;
retrySend:
if (connectionState.ConnectionAttempts > 1)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogInformation(
"Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {Payload}",
connectionState.ConnectionAttempts,
_retryAttempts, parameters);
}
}
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
DontFragment = false,
@ -210,53 +205,43 @@ namespace Integrations.Cod
ExclusiveAddressUse = true,
})
{
// wait for send to be ready
try
if (!token.IsCancellationRequested)
{
await connectionState.OnSentData.WaitAsync(token);
}
catch (OperationCanceledException)
{
_log.LogDebug("OnSent did not complete in time");
throw new RConException("Timed out waiting for access to RCon send socket");
connectionState.ConnectionAttempts++;
}
// wait for receive to be ready
try
{
await connectionState.OnReceivedData.WaitAsync(token);
}
catch (OperationCanceledException)
{
_log.LogDebug("OnReceived did not complete in time");
if (connectionState.OnSentData.CurrentCount == 0)
{
connectionState.OnSentData.Release();
}
throw new RConException("Timed out waiting for access to RCon receive socket");
}
connectionState.SendEventArgs.UserToken = new ConnectionUserToken
{
Socket = socket,
CancellationToken = token
};
connectionState.ConnectionAttempts++;
connectionState.BytesReadPerSegment.Clear();
connectionState.ReceivedBytes.Clear();
_log.LogDebug(
"Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures})",
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts);
"Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures}) parameters {Payload}",
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts, parameters);
try
{
connectionState.LastQuery = DateTime.Now;
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse,
timeout ?? TimeSpan.Zero, token);
var retryTimeout = StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts);
var overrideTimeout = _parser.OverrideTimeoutForCommand(parameters);
var maxTimeout = !overrideTimeout.HasValue || overrideTimeout == TimeSpan.Zero
? retryTimeout
: overrideTimeout.Value;
using var internalTokenSource = new CancellationTokenSource(maxTimeout);
using var chainedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(token, internalTokenSource.Token);
if (connectionState.ConnectionAttempts > 1)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogInformation(
"Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts, {Timeout}ms timeout) with parameters {Payload}",
connectionState.ConnectionAttempts, _retryAttempts,
maxTimeout.TotalMilliseconds, parameters);
}
}
waitForResponse = waitForResponse && overrideTimeout.HasValue;
response = await SendPayloadAsync(socket, payload, waitForResponse, chainedTokenSource.Token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{
@ -269,26 +254,23 @@ namespace Integrations.Cod
catch (OperationCanceledException)
{
_log.LogDebug("OperationCanceledException when waiting for payload send to complete");
// if we timed out due to the cancellation token,
// we don't want to count that as an attempt
_log.LogDebug("OperationCanceledException when waiting for payload send to complete");
connectionState.ConnectionAttempts = 0;
}
catch
{
// we want to retry with a delay
if (connectionState.ConnectionAttempts < _retryAttempts)
if (token.IsCancellationRequested)
{
try
if (connectionState.ConnectionAttempts > 0)
{
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token);
}
catch (OperationCanceledException)
{
_log.LogDebug("OperationCancelled while waiting for retry");
throw;
connectionState.ConnectionAttempts--;
}
throw new RConException("Timed out waiting on retry delay for RCon socket",
token.IsCancellationRequested);
}
if (connectionState.ConnectionAttempts < _retryAttempts)
{
goto retrySend;
}
@ -302,25 +284,24 @@ namespace Integrations.Cod
connectionState.ConnectionAttempts = 0;
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
}
finally
catch (Exception ex)
{
try
{
if (connectionState.OnSentData.CurrentCount == 0)
{
connectionState.OnSentData.Release();
}
_log.LogDebug(ex, "RCon Exception");
if (connectionState.OnReceivedData.CurrentCount == 0)
{
connectionState.OnReceivedData.Release();
}
}
catch
if (connectionState.ConnectionAttempts < _retryAttempts)
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
goto retrySend;
}
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Made {ConnectionAttempts} attempts to send RCon data to server, but received no response",
connectionState.ConnectionAttempts);
}
connectionState.ConnectionAttempts = 0;
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
}
}
@ -336,6 +317,81 @@ namespace Integrations.Cod
? ReassembleSegmentedStatus(response)
: RecombineMessages(response);
var validatedResponse = ValidateResponse(type, responseString);
return validatedResponse;
}
private async Task<byte[][]> SendPayloadAsync(Socket rconSocket, byte[] payload, bool waitForResponse,
CancellationToken token = default)
{
var connectionState = ActiveQueries[Endpoint];
if (rconSocket is null)
{
_log.LogDebug("Invalid state");
throw new InvalidOperationException("State is not valid for socket operation");
}
var sentByteCount = await rconSocket.SendToAsync(payload, SocketFlags.None, Endpoint, token);
var complete = sentByteCount == payload.Length;
if (!complete)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Could not send data to remote RCon socket on attempt #{ConnectionAttempts}",
connectionState.ConnectionAttempts);
}
rconSocket.Close();
throw new NetworkException("Could not send data to remote RCon socket", rconSocket);
}
if (!waitForResponse)
{
return Array.Empty<byte[]>();
}
_log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}",
connectionState.ConnectionAttempts);
await ReceiveAndStoreSocketData(rconSocket, token, connectionState);
if (_parser.GameName == Server.Game.IW3)
{
await Task.Delay(100, token); // CoD4x shenanigans
}
while (rconSocket.Available > 0)
{
await ReceiveAndStoreSocketData(rconSocket, token, connectionState);
}
rconSocket.Close();
return GetResponseData(connectionState);
}
private async Task ReceiveAndStoreSocketData(Socket rconSocket, CancellationToken token,
ConnectionState connectionState)
{
var result = await rconSocket.ReceiveFromAsync(connectionState.ReceiveBuffer,
SocketFlags.None, Endpoint, token);
if (result.ReceivedBytes == 0)
{
return;
}
var storageBuffer = new byte[result.ReceivedBytes];
Array.Copy(connectionState.ReceiveBuffer, storageBuffer, storageBuffer.Length);
connectionState.ReceivedBytes.Add(storageBuffer);
}
#region Helpers
private string[] ValidateResponse(StaticHelpers.QueryType type, string responseString)
{
// note: not all games respond if the password is wrong or not set
if (responseString.Contains("Invalid password", StringComparison.InvariantCultureIgnoreCase) ||
responseString.Contains("rconpassword"))
@ -359,21 +415,19 @@ namespace Integrations.Cod
? _config.CommandPrefixes.RconGetInfoResponseHeader
: responseHeaderMatch);
if (headerSplit.Length != 2)
if (headerSplit.Length == 2)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Invalid response header from server. Expected {Expected}, but got {Response}",
_config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault());
}
throw new RConException("Unexpected response header from server");
return headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
}
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
return splitResponse;
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Invalid response header from server. Expected {Expected}, but got {Response}",
_config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault());
}
throw new RConException("Unexpected response header from server");
}
/// <summary>
@ -404,7 +458,7 @@ namespace Integrations.Cod
return string.Join("", splitStatusStrings);
}
/// <summary>
/// Recombines multiple game messages into one
/// </summary>
@ -433,269 +487,11 @@ namespace Integrations.Cod
return builder.ToString();
}
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout,
CancellationToken token = default)
{
var connectionState = ActiveQueries[Endpoint];
var rconSocket = ((ConnectionUserToken)connectionState.SendEventArgs.UserToken)?.Socket;
if (rconSocket is null)
{
_log.LogDebug("Invalid state");
throw new InvalidOperationException("State is not valid for socket operation");
}
if (connectionState.ReceiveEventArgs.RemoteEndPoint == null &&
connectionState.SendEventArgs.RemoteEndPoint == null)
{
// setup the event handlers only once because we're reusing the event args
connectionState.SendEventArgs.Completed += OnDataSent;
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
connectionState.SendEventArgs.RemoteEndPoint = Endpoint;
connectionState.ReceiveEventArgs.RemoteEndPoint = Endpoint;
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
{
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server
var sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
if (sendDataPending)
{
// the send has not been completed asynchronously
// this really shouldn't ever happen because it's UDP
var complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(4), token);
if (!complete)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Socket timed out while sending RCon data on attempt {Attempt}",
connectionState.ConnectionAttempts);
}
rconSocket.Close();
throw new NetworkException("Timed out sending RCon data", rconSocket);
}
}
if (!waitForResponse)
{
return Array.Empty<byte[]>();
}
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
// get our response back
var receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
if (receiveDataPending)
{
_log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}",
connectionState.ConnectionAttempts);
var completed = false;
try
{
completed = await connectionState.OnReceivedData.WaitAsync(
new[]
{
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts),
overrideTimeout
}.Max(), token);
}
catch (OperationCanceledException)
{
// ignored
}
if (!completed)
{
if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Socket timed out while waiting for RCon response on attempt {Attempt} with timeout delay of {Timeout}",
connectionState.ConnectionAttempts,
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts));
}
}
rconSocket.Close();
_log.LogDebug("OnDataReceived did not complete in allocated time");
throw new NetworkException("Timed out receiving RCon response", rconSocket);
}
}
rconSocket.Close();
return GetResponseData(connectionState);
}
private static byte[][] GetResponseData(ConnectionState connectionState)
{
var responseList = new List<byte[]>();
var totalBytesRead = 0;
foreach (var bytesRead in connectionState.BytesReadPerSegment)
{
responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead)
.Take(bytesRead)
.ToArray());
totalBytesRead += bytesRead;
}
return responseList.ToArray();
}
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{
_log.LogDebug("Read {BytesTransferred} bytes from {Endpoint}", e.BytesTransferred,
e.RemoteEndPoint?.ToString());
// this occurs when we close the socket
if (e.BytesTransferred == 0)
{
_log.LogDebug("No bytes were transmitted so the connection was probably closed");
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
return;
}
var state = ActiveQueries[Endpoint];
var cancellationRequested = ((ConnectionUserToken)e.UserToken)?.CancellationToken.IsCancellationRequested ??
false;
if (sender is not Socket sock || cancellationRequested)
{
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
return;
}
state.BytesReadPerSegment.Add(e.BytesTransferred);
// I don't even want to know why this works for getting more data from Cod4x
// but I'm leaving it in here as long as it doesn't break anything.
// it's very stupid...
Thread.Sleep(150);
try
{
var totalBytesTransferred = e.BytesTransferred;
_log.LogDebug("{Total} total bytes transferred with {Available} bytes remaining", totalBytesTransferred,
sock.Available);
// we still have available data so the payload was segmented
while (sock.Available > 0)
{
_log.LogDebug("{Available} more bytes to be read", sock.Available);
var bufferSpaceAvailable = sock.Available + totalBytesTransferred - state.ReceiveBuffer.Length;
if (bufferSpaceAvailable >= 0)
{
_log.LogWarning(
"Not enough buffer space to store incoming data {BytesNeeded} additional bytes required",
bufferSpaceAvailable);
continue;
}
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
if (sock.ReceiveAsync(state.ReceiveEventArgs))
{
_log.LogDebug("Remaining bytes are async");
continue;
}
_log.LogDebug("Read {BytesTransferred} synchronous bytes from {Endpoint}",
state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint?.ToString());
// we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
}
}
catch (ObjectDisposedException)
{
_log.LogDebug("Socket was disposed while receiving data");
}
finally
{
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
}
}
private void OnDataSent(object sender, SocketAsyncEventArgs e)
{
_log.LogDebug("Sent {ByteCount} bytes to {Endpoint}", e.Buffer?.Length,
e.ConnectSocket?.RemoteEndPoint?.ToString());
var semaphore = ActiveQueries[Endpoint].OnSentData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
return connectionState.ReceivedBytes.ToArray();
}
#endregion
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
namespace Integrations.Cod
@ -13,26 +12,13 @@ namespace Integrations.Cod
~ConnectionState()
{
OnComplete.Dispose();
OnSentData.Dispose();
OnReceivedData.Dispose();
}
public int ConnectionAttempts { get; set; }
private const int BufferSize = 16384;
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new(1, 1);
public readonly SemaphoreSlim OnSentData = new(1, 1);
public readonly SemaphoreSlim OnReceivedData = new (1, 1);
public List<int> BytesReadPerSegment { get; set; } = new();
public SocketAsyncEventArgs SendEventArgs { get; set; } = new();
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new();
public List<byte[]> ReceivedBytes { get; } = new();
public DateTime LastQuery { get; set; } = DateTime.Now;
}
internal class ConnectionUserToken
{
public Socket Socket { get; set; }
public CancellationToken CancellationToken { get; set; }
}
}

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -26,49 +26,22 @@ namespace IW4MAdmin.Plugins.Login
_configHandler = configurationHandlerFactory.GetConfigurationHandler<Configuration>("LoginPluginSettings");
}
public Task OnEventAsync(GameEvent E, Server S)
public Task OnEventAsync(GameEvent gameEvent, Server server)
{
if (E.IsRemote || _configHandler.Configuration().RequirePrivilegedClientLogin == false)
if (gameEvent.IsRemote || _configHandler.Configuration().RequirePrivilegedClientLogin == false)
return Task.CompletedTask;
if (E.Type == GameEvent.EventType.Connect)
if (gameEvent.Type == GameEvent.EventType.Connect)
{
AuthorizedClients.TryAdd(E.Origin.ClientId, false);
E.Origin.SetAdditionalProperty("IsLoggedIn", false);
AuthorizedClients.TryAdd(gameEvent.Origin.ClientId, false);
gameEvent.Origin.SetAdditionalProperty("IsLoggedIn", false);
}
if (E.Type == GameEvent.EventType.Disconnect)
if (gameEvent.Type == GameEvent.EventType.Disconnect)
{
AuthorizedClients.TryRemove(E.Origin.ClientId, out bool value);
AuthorizedClients.TryRemove(gameEvent.Origin.ClientId, out _);
}
if (E.Type == GameEvent.EventType.Command)
{
if (E.Origin.Level < EFClient.Permission.Moderator ||
E.Origin.Level == EFClient.Permission.Console)
return Task.CompletedTask;
if (E.Extra.GetType() == typeof(SetPasswordCommand) &&
E.Origin?.Password == null)
return Task.CompletedTask;
if (E.Extra.GetType() == typeof(LoginCommand))
return Task.CompletedTask;
if (E.Extra.GetType() == typeof(RequestTokenCommand))
return Task.CompletedTask;
if (!AuthorizedClients[E.Origin.ClientId])
{
throw new AuthorizationException(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_AUTH"]);
}
else
{
E.Origin.SetAdditionalProperty("IsLoggedIn", true);
}
}
return Task.CompletedTask;
}
@ -76,6 +49,36 @@ namespace IW4MAdmin.Plugins.Login
{
AuthorizedClients = new ConcurrentDictionary<int, bool>();
manager.CommandInterceptors.Add(gameEvent =>
{
if (gameEvent.Type != GameEvent.EventType.Command || gameEvent.Extra is null || gameEvent.IsRemote)
{
return true;
}
if (gameEvent.Origin.Level < EFClient.Permission.Moderator ||
gameEvent.Origin.Level == EFClient.Permission.Console)
return true;
if (gameEvent.Extra.GetType() == typeof(SetPasswordCommand) &&
gameEvent.Origin?.Password == null)
return true;
if (gameEvent.Extra.GetType() == typeof(LoginCommand))
return true;
if (gameEvent.Extra.GetType() == typeof(RequestTokenCommand))
return true;
if (!AuthorizedClients[gameEvent.Origin.ClientId])
{
return false;
}
gameEvent.Origin.SetAdditionalProperty("IsLoggedIn", true);
return true;
});
await _configHandler.BuildAsync();
if (_configHandler.Configuration() == null)
{

View File

@ -0,0 +1,55 @@
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class MuteCommand : Command
{
public MuteCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "mute";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_DESC"];
Alias = "mu";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Origin.ClientId == gameEvent.Target.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]);
return;
}
if (await Plugin.MuteManager.Mute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, null, gameEvent.Data))
{
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_MUTED"]
.FormatExt(gameEvent.Target.CleanedName));
gameEvent.Target.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_TARGET_MUTED"]
.FormatExt(gameEvent.Data));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_NOT_UNMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
}
}

View File

@ -0,0 +1,50 @@
using Data.Models.Client;
using Humanizer;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class MuteInfoCommand : Command
{
public MuteInfoCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "muteinfo";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_MUTEINFO_DESC"];
Alias = "mi";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var currentMuteMeta = await Plugin.MuteManager.GetCurrentMuteState(gameEvent.Target);
switch (currentMuteMeta.MuteState)
{
case MuteState.Muted when currentMuteMeta.Expiration is null:
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTEINFO_SUCCESS"]
.FormatExt(gameEvent.Target.Name, currentMuteMeta.Reason));
return;
case MuteState.Muted when currentMuteMeta.Expiration.HasValue && currentMuteMeta.Expiration.Value > DateTime.UtcNow:
var remainingTime = (currentMuteMeta.Expiration.Value - DateTime.UtcNow).HumanizeForCurrentCulture();
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTEINFO_TM_SUCCESS"]
.FormatExt(gameEvent.Target.Name, currentMuteMeta.Reason, remainingTime));
return;
default:
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTEINFO_NONE"]);
break;
}
}
}

View File

@ -0,0 +1,73 @@
using System.Text.RegularExpressions;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class TempMuteCommand : Command
{
private const string TempBanRegex = @"([0-9]+\w+)\ (.+)";
public TempMuteCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "tempmute";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_DESC"];
Alias = "tm";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_DURATION"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Origin.ClientId == gameEvent.Target.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]);
return;
}
var match = Regex.Match(gameEvent.Data, TempBanRegex);
if (match.Success)
{
var expiration = DateTime.UtcNow + match.Groups[1].ToString().ParseTimespan();
var reason = match.Groups[2].ToString();
if (await Plugin.MuteManager.Mute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, expiration, reason))
{
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_TEMPMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
gameEvent.Target.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_TARGET_TEMPMUTED"]
.FormatExt(expiration.HumanizeForCurrentCulture(), reason));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_NOT_UNMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_BAD_FORMAT"]);
}
}

View File

@ -0,0 +1,55 @@
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class UnmuteCommand : Command
{
public UnmuteCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "unmute";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_DESC"];
Alias = "um";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Origin.ClientId == gameEvent.Target.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]);
return;
}
if (await Plugin.MuteManager.Unmute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, gameEvent.Data))
{
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_UNMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
gameEvent.Target.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_TARGET_UNMUTED"]
.FormatExt(gameEvent.Data));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_NOT_MUTED"]
.FormatExt(gameEvent.Target.CleanedName));
}
}

20
Plugins/Mute/Mute.csproj Normal file
View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>MrAmos123</Authors>
<OutputType>Library</OutputType>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

206
Plugins/Mute/MuteManager.cs Normal file
View File

@ -0,0 +1,206 @@
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using static System.Enum;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Mute;
public class MuteManager
{
private readonly IMetaServiceV2 _metaService;
private readonly ITranslationLookup _translationLookup;
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _databaseContextFactory;
private readonly SemaphoreSlim _onMuteAction = new(1, 1);
public MuteManager(IServiceProvider serviceProvider)
{
_metaService = serviceProvider.GetRequiredService<IMetaServiceV2>();
_translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
_logger = serviceProvider.GetRequiredService<ILogger<MuteManager>>();
_databaseContextFactory = serviceProvider.GetRequiredService<IDatabaseContextFactory>();
}
public static bool IsExpiredMute(MuteStateMeta muteStateMeta) =>
muteStateMeta.Expiration is not null && muteStateMeta.Expiration < DateTime.UtcNow;
public async Task<MuteStateMeta> GetCurrentMuteState(EFClient client)
{
try
{
await _onMuteAction.WaitAsync();
var clientMuteMeta = await ReadPersistentDataV2(client);
if (clientMuteMeta is not null) return clientMuteMeta;
// Return null if the client doesn't have old or new meta.
var muteState = await ReadPersistentDataV1(client);
clientMuteMeta = new MuteStateMeta
{
Reason = muteState is null ? string.Empty : _translationLookup["PLUGINS_MUTE_MIGRATED"],
Expiration = muteState switch
{
null => DateTime.UtcNow,
MuteState.Muted => null,
_ => DateTime.UtcNow
},
MuteState = muteState ?? MuteState.Unmuted,
CommandExecuted = true
};
// Migrate old mute meta, else, client has no state, so set a generic one, but don't write it to database.
if (muteState is not null)
{
clientMuteMeta.CommandExecuted = false;
await WritePersistentData(client, clientMuteMeta);
await CreatePenalty(muteState.Value, Utilities.IW4MAdminClient(), client, clientMuteMeta.Expiration,
clientMuteMeta.Reason);
}
else
{
client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
}
return clientMuteMeta;
}
finally
{
if (_onMuteAction.CurrentCount == 0) _onMuteAction.Release();
}
}
public async Task<bool> Mute(Server server, EFClient origin, EFClient target, DateTime? dateTime, string reason)
{
var clientMuteMeta = await GetCurrentMuteState(target);
if (clientMuteMeta.MuteState is MuteState.Muted && clientMuteMeta.CommandExecuted) return false;
clientMuteMeta = new MuteStateMeta
{
Expiration = dateTime,
MuteState = MuteState.Muted,
Reason = reason,
CommandExecuted = false
};
await WritePersistentData(target, clientMuteMeta);
await CreatePenalty(MuteState.Muted, origin, target, dateTime, reason);
// Handle game command
var client = server.GetClientsAsList().FirstOrDefault(client => client.NetworkId == target.NetworkId);
await PerformGameCommand(server, client, clientMuteMeta);
return true;
}
public async Task<bool> Unmute(Server server, EFClient origin, EFClient target, string reason)
{
var clientMuteMeta = await GetCurrentMuteState(target);
if (clientMuteMeta.MuteState is MuteState.Unmuted && clientMuteMeta.CommandExecuted) return false;
if (!target.IsIngame && clientMuteMeta.MuteState is MuteState.Unmuting) return false;
if (clientMuteMeta.MuteState is not MuteState.Unmuting && origin.ClientId != 1)
{
await CreatePenalty(MuteState.Unmuted, origin, target, DateTime.UtcNow, reason);
}
await ExpireMutePenalties(target);
clientMuteMeta = new MuteStateMeta
{
Expiration = DateTime.UtcNow,
MuteState = target.IsIngame ? MuteState.Unmuted : MuteState.Unmuting,
Reason = reason,
CommandExecuted = false
};
await WritePersistentData(target, clientMuteMeta);
// Handle game command
var client = server.GetClientsAsList().FirstOrDefault(client => client.NetworkId == target.NetworkId);
await PerformGameCommand(server, client, clientMuteMeta);
return true;
}
private async Task CreatePenalty(MuteState muteState, EFClient origin, EFClient target, DateTime? dateTime,
string reason)
{
var newPenalty = new EFPenalty
{
Type = muteState is MuteState.Unmuted ? EFPenalty.PenaltyType.Unmute :
dateTime is null ? EFPenalty.PenaltyType.Mute : EFPenalty.PenaltyType.TempMute,
Expires = muteState is MuteState.Unmuted ? DateTime.UtcNow : dateTime,
Offender = target,
Offense = reason,
Punisher = origin,
Link = target.AliasLink
};
_logger.LogDebug("Creating new {MuteState} Penalty for {Target} with reason {Reason}",
nameof(muteState), target.Name, reason);
await newPenalty.TryCreatePenalty(Plugin.Manager.GetPenaltyService(), _logger);
}
private async Task ExpireMutePenalties(EFClient client)
{
await using var context = _databaseContextFactory.CreateContext();
var mutePenalties = await context.Penalties
.Where(penalty => penalty.OffenderId == client.ClientId &&
(penalty.Type == EFPenalty.PenaltyType.Mute ||
penalty.Type == EFPenalty.PenaltyType.TempMute) &&
(penalty.Expires == null || penalty.Expires > DateTime.UtcNow))
.ToListAsync();
foreach (var mutePenalty in mutePenalties)
{
mutePenalty.Expires = DateTime.UtcNow;
}
await context.SaveChangesAsync();
}
public static async Task PerformGameCommand(Server server, EFClient? client, MuteStateMeta muteStateMeta)
{
if (client is null || !client.IsIngame) return;
switch (muteStateMeta.MuteState)
{
case MuteState.Muted:
await server.ExecuteCommandAsync($"muteClient {client.ClientNumber}");
muteStateMeta.CommandExecuted = true;
break;
case MuteState.Unmuted:
await server.ExecuteCommandAsync($"unmute {client.ClientNumber}");
muteStateMeta.CommandExecuted = true;
break;
}
}
private async Task<MuteState?> ReadPersistentDataV1(EFClient client) => TryParse<MuteState>(
(await _metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value,
out var muteState)
? muteState
: null;
private async Task<MuteStateMeta?> ReadPersistentDataV2(EFClient client)
{
// Get meta from client
var clientMuteMeta = client.GetAdditionalProperty<MuteStateMeta>(Plugin.MuteKey);
if (clientMuteMeta is not null) return clientMuteMeta;
// Get meta from database and store in client if exists
clientMuteMeta = await _metaService.GetPersistentMetaValue<MuteStateMeta>(Plugin.MuteKey, client.ClientId);
if (clientMuteMeta is not null) client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
return clientMuteMeta;
}
private async Task WritePersistentData(EFClient client, MuteStateMeta clientMuteMeta)
{
client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
await _metaService.SetPersistentMetaValue(Plugin.MuteKey, clientMuteMeta, client.ClientId);
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Mute;
public class MuteStateMeta
{
public string? Reason { get; set; }
public DateTime? Expiration { get; set; }
public MuteState MuteState { get; set; }
[JsonIgnore] public bool CommandExecuted { get; set; }
}
public enum MuteState
{
Muted,
Unmuting,
Unmuted
}

311
Plugins/Mute/Plugin.cs Normal file
View File

@ -0,0 +1,311 @@
using Data.Abstractions;
using Microsoft.Extensions.Logging;
using Mute.Commands;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Mute;
public class Plugin : IPlugin
{
public string Name => "Mute";
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "Amos";
public const string MuteKey = "IW4MMute";
public static MuteManager MuteManager { get; private set; } = null!;
public static IManager Manager { get; private set; } = null!;
public static readonly Server.Game[] SupportedGames = {Server.Game.IW4};
private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"};
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private static readonly string MuteInteraction = "Webfront::Profile::Mute";
public Plugin(ILogger<Plugin> logger, IInteractionRegistration interactionRegistration,
IRemoteCommandService remoteCommandService, IServiceProvider serviceProvider)
{
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
MuteManager = new MuteManager(serviceProvider);
}
public async Task OnEventAsync(GameEvent gameEvent, Server server)
{
if (!SupportedGames.Contains(server.GameName)) return;
switch (gameEvent.Type)
{
case GameEvent.EventType.Join:
// Check if user has any meta set, else ignore (unmuted)
var muteMetaJoin = await MuteManager.GetCurrentMuteState(gameEvent.Origin);
switch (muteMetaJoin.MuteState)
{
case MuteState.Muted:
// Let the client know when their mute expires.
gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt(
muteMetaJoin.Expiration is not null
? muteMetaJoin.Expiration.Value.HumanizeForCurrentCulture()
: Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"],
muteMetaJoin.Reason));
break;
case MuteState.Unmuting:
// Handle unmute of unmuted players.
await MuteManager.Unmute(server, Utilities.IW4MAdminClient(), gameEvent.Origin,
muteMetaJoin.Reason ?? string.Empty);
gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_COMMANDS_UNMUTE_TARGET_UNMUTED"]
.FormatExt(muteMetaJoin.Reason));
break;
}
break;
case GameEvent.EventType.Say:
var muteMetaSay = await MuteManager.GetCurrentMuteState(gameEvent.Origin);
switch (muteMetaSay.MuteState)
{
case MuteState.Muted:
// Let the client know when their mute expires.
gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt(
muteMetaSay.Expiration is not null
? muteMetaSay.Expiration.Value.HumanizeForCurrentCulture()
: Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"],
muteMetaSay.Reason));
break;
}
break;
case GameEvent.EventType.Update:
// Get correct EFClient object
var client = server.GetClientsAsList()
.FirstOrDefault(client => client.NetworkId == gameEvent.Origin.NetworkId);
if (client == null) break;
var muteMetaUpdate = await MuteManager.GetCurrentMuteState(client);
if (!muteMetaUpdate.CommandExecuted)
{
await MuteManager.PerformGameCommand(server, client, muteMetaUpdate);
}
switch (muteMetaUpdate.MuteState)
{
case MuteState.Muted:
// Handle unmute if expired.
if (MuteManager.IsExpiredMute(muteMetaUpdate))
{
await MuteManager.Unmute(server, Utilities.IW4MAdminClient(), client,
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_EXPIRED"]);
client.Tell(
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_TARGET_EXPIRED"]);
}
break;
}
break;
}
}
public Task OnLoadAsync(IManager manager)
{
Manager = manager;
manager.CommandInterceptors.Add(gameEvent =>
{
if (gameEvent.Extra is not Command command)
{
return true;
}
var muteMeta = MuteManager.GetCurrentMuteState(gameEvent.Origin).GetAwaiter().GetResult();
if (muteMeta.MuteState is not MuteState.Muted)
{
return true;
}
return !DisabledCommands.Contains(command.GetType().Name) && !command.IsBroadcast;
});
_interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, token) =>
{
if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value))
{
return null;
}
var clientMuteMetaState =
(await MuteManager.GetCurrentMuteState(new EFClient {ClientId = targetClientId.Value}))
.MuteState;
var server = manager.GetServers().First();
string GetCommandName(Type commandType) =>
manager.Commands.FirstOrDefault(command => command.GetType() == commandType)?.Name ?? "";
return clientMuteMetaState is MuteState.Unmuted or MuteState.Unmuting
? CreateMuteInteraction(targetClientId.Value, server, GetCommandName)
: CreateUnmuteInteraction(targetClientId.Value, server, GetCommandName);
});
return Task.CompletedTask;
}
private InteractionData CreateMuteInteraction(int targetClientId, Server server,
Func<Type, string> getCommandNameFunc)
{
var reasonInput = new
{
Name = "Reason",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_REASON"],
Type = "text",
Values = (Dictionary<string, string>?)null
};
var durationInput = new
{
Name = "Duration",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_DURATION"],
Type = "select",
Values = (Dictionary<string, string>?)new Dictionary<string, string>
{
{"5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture()},
{"30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture()},
{"1h", TimeSpan.FromHours(1).HumanizeForCurrentCulture()},
{"6h", TimeSpan.FromHours(6).HumanizeForCurrentCulture()},
{"1d", TimeSpan.FromDays(1).HumanizeForCurrentCulture()},
{"p", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_SELECTION_PERMANENT"]}
}
};
var inputs = new[] {reasonInput, durationInput};
var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData
{
EntityId = targetClientId,
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"],
DisplayMeta = "oi-volume-off",
ActionPath = "DynamicAction",
ActionMeta = new()
{
{"InteractionId", MuteInteraction},
{"Inputs", inputsJson},
{
"ActionButtonLabel",
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"]
},
{
"Name",
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"]
},
{"ShouldRefresh", true.ToString()}
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name,
Action = async (originId, targetId, gameName, meta, cancellationToken) =>
{
if (!targetId.HasValue)
{
return "No target client id specified";
}
var isTempMute = meta.ContainsKey(durationInput.Name) &&
meta[durationInput.Name] != durationInput.Values?.Last().Key;
var muteCommand = getCommandNameFunc(isTempMute ? typeof(TempMuteCommand) : typeof(MuteCommand));
var args = new List<string>();
if (meta.TryGetValue(durationInput.Name, out var duration) &&
duration != durationInput.Values?.Last().Key)
{
args.Add(duration);
}
if (meta.TryGetValue(reasonInput.Name, out var reason))
{
args.Add(reason);
}
var commandResponse =
await _remoteCommandService.Execute(originId, targetId, muteCommand, args, server);
return string.Join(".", commandResponse.Select(result => result.Response));
}
};
}
private InteractionData CreateUnmuteInteraction(int targetClientId, Server server,
Func<Type, string> getCommandNameFunc)
{
var reasonInput = new
{
Name = "Reason",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_REASON"],
Type = "text",
};
var inputs = new[] {reasonInput};
var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData
{
EntityId = targetClientId,
Name = Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
DisplayMeta = "oi-volume-high",
ActionPath = "DynamicAction",
ActionMeta = new()
{
{"InteractionId", MuteInteraction},
{"Outputs", reasonInput.Name},
{"Inputs", inputsJson},
{
"ActionButtonLabel",
Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"]
},
{
"Name",
Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"]
},
{"ShouldRefresh", true.ToString()}
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name,
Action = async (originId, targetId, gameName, meta, cancellationToken) =>
{
if (!targetId.HasValue)
{
return "No target client id specified";
}
var args = new List<string>();
if (meta.TryGetValue(reasonInput.Name, out var reason))
{
args.Add(reason);
}
var commandResponse =
await _remoteCommandService.Execute(originId, targetId, getCommandNameFunc(typeof(UnmuteCommand)),
args, server);
return string.Join(".", commandResponse.Select(result => result.Response));
}
};
}
public Task OnUnloadAsync()
{
_interactionRegistration.UnregisterInteraction(MuteInteraction);
return Task.CompletedTask;
}
public Task OnTickAsync(Server server)
{
return Task.CompletedTask;
}
}

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -72,27 +72,27 @@ let plugin = {
};
let commands = [{
name: 'giveweapon',
description: 'gives specified weapon',
alias: 'gw',
permission: 'SeniorAdmin',
targetRequired: true,
arguments: [{
name: 'player',
required: true
},
{
name: 'weapon name',
name: 'giveweapon',
description: 'gives specified weapon',
alias: 'gw',
permission: 'SeniorAdmin',
targetRequired: true,
arguments: [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
},
{
name: 'weapon name',
required: true
}],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'GiveWeapon', gameEvent.Origin, gameEvent.Target, {weaponName: gameEvent.Data});
}
sendScriptCommand(gameEvent.Owner, 'GiveWeapon', gameEvent.Origin, gameEvent.Target, {weaponName: gameEvent.Data});
}
},
},
{
name: 'takeweapons',
description: 'take all weapons from specified player',
@ -103,7 +103,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -121,7 +121,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -139,7 +139,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -147,24 +147,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'LockControls', gameEvent.Origin, gameEvent.Target, undefined);
}
},
{
name: 'unlockcontrols',
description: 'unlocks target player\'s controls',
alias: 'ulc',
permission: 'Administrator',
targetRequired: true,
arguments: [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'UnlockControls', gameEvent.Origin, gameEvent.Target, undefined);
}
},
{
name: 'noclip',
description: 'enable noclip on yourself ingame',
@ -180,21 +162,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'NoClip', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'noclipoff',
description: 'disable noclip on yourself ingame',
alias: 'nco',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'NoClipOff', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'hide',
description: 'hide yourself ingame',
@ -202,7 +169,7 @@ let commands = [{
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -210,21 +177,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'Hide', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'unhide',
description: 'unhide yourself ingame',
alias: 'unh',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'Unhide', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'alert',
description: 'alert a player',
@ -239,7 +191,7 @@ let commands = [{
name: 'message',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -260,7 +212,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -278,7 +230,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -304,7 +256,7 @@ let commands = [{
name: 'z',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -328,7 +280,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -336,21 +288,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'Kill', gameEvent.Origin, gameEvent.Target, undefined);
}
},
{
name: 'nightmode',
description: 'sets server into nightmode',
alias: 'nitem',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'NightMode', gameEvent.Origin, undefined, undefined);
}
},
{
name: 'setspectator',
description: 'sets a player as spectator',
@ -361,7 +298,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -437,6 +374,15 @@ const initialize = (server) => {
return true;
}
const getClientStats = (client, server) => {
const contextFactory = _serviceResolver.ResolveService('IDatabaseContextFactory');
const context = contextFactory.CreateContext(false);
const stats = context.ClientStatistics.GetClientsStatData([client.ClientId], server.GetId()); // .Find(client.ClientId, serverId);
context.Dispose();
return stats.length > 0 ? stats[0] : undefined;
}
function onReceivedDvar(server, dvarName, dvarValue, success) {
const logger = _serviceResolver.ResolveService('ILogger');
logger.WriteDebug(`Received ${dvarName}=${dvarValue} success=${success}`);
@ -478,15 +424,21 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
let data = [];
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const meta = metaService.GetPersistentMeta(event.data, client, token).GetAwaiter().GetResult();
const meta = metaService.GetPersistentMeta(event.data, client.ClientId, token).GetAwaiter().GetResult();
data[event.data] = meta === null ? '' : meta.Value;
logger.WriteDebug(`event data is ${event.data}`);
} else {
const clientStats = getClientStats(client, server);
const tagMeta = metaService.GetPersistentMetaByLookup('ClientTagV2', 'ClientTagNameV2', client.ClientId, token).GetAwaiter().GetResult();
data = {
level: client.Level,
clientId: client.ClientId,
lastConnection: client.LastConnection
lastConnection: client.LastConnection,
tag: tagMeta?.Value ?? '',
performance: clientStats?.Performance ?? 200.0
};
}
@ -515,13 +467,15 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
} else {
if (event.subType === 'Meta') {
try {
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
if (event.data['direction'] != null) {
event.data['direction'] = 'up'
? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
} else {
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
if (event.data['value'] != null && event.data['key'] != null) {
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
if (event.data['direction'] != null) {
event.data['direction'] = 'up'
? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
} else {
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
}
}
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Complete'});
} catch (error) {
@ -559,11 +513,13 @@ const pollForEvents = server => {
}
if (server.Throttled) {
logger.WriteDebug('Server is throttled so we are not polling for game data');
return;
}
if (!state.waitingOnInput) {
state.waitingOnInput = true;
logger.WriteDebug('Attempting to get in dvar value');
getDvar(server, inDvar, onReceivedDvar);
}
@ -627,7 +583,7 @@ const parseDataString = data => {
dict[keyValue[0]] = keyValue[1];
}
return dict.length === 0 ? data : dict;
return Object.keys(dict).length === 0 ? data : dict;
}
const validateEnabled = (server, origin) => {

View File

@ -0,0 +1,52 @@
var rconParser;
var eventParser;
var plugin = {
author: 'Diamante',
version: 0.2,
name: 'BOIII Parser',
isParser: true,
onEventAsync: function(gameEvent, server) {},
onLoadAsync: function(manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown)(?:\\(\\d+\\))? +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport *';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff(\1|print) ?';
rconParser.Configuration.GametypeStatus.Pattern = 'Gametype: (.+)';
rconParser.Configuration.MapStatus.Pattern = 'Map: (.+)';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // disables this, because it's useless on T7/BOIII
rconParser.Configuration.ServerNotRunningResponse = 'this is here to prevent a hibernating server from being detected as not running';
rconParser.Configuration.DefaultRConPort = 27017;
rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'live_steam_server_name');
rconParser.Configuration.OverrideDvarNameMapping.Add('g_password', 'live_steam_server_password');
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.DefaultDvarValues.Add('g_gametype', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basegame', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_homepath', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_game', '');
rconParser.Configuration.Status.AddMapping(105, 6); // ip address
rconParser.Configuration.GametypeStatus.AddMapping(112, 1); // gametype
rconParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef';
rconParser.GameName = 8; // BO3
rconParser.CanGenerateLogPath = false;
eventParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef';
eventParser.GameName = 8; // BO3
eventParser.Configuration.GameDirectory = 'usermaps';
eventParser.Configuration.Say.Pattern = '^(chat|chatteam);(?:[0-9]+);([0-9]+);([0-9]+);(.+);(.*)$';
},
onUnloadAsync: function() {},
onTickAsync: function(server) {}
};

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'FrenchFry, RaidMax',
version: 0.8,
version: 0.9,
name: 'CoD4x Parser',
isParser: true,
@ -15,7 +15,7 @@ var plugin = {
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +playerid +steamid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|0) +([[0-9]+|0]) +(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|bot) +(-*[0-9]+) +([0-9]+) *$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|0) +([[0-9]+|0]) +(.{0,34}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|bot) +(-*[0-9]+) +([0-9]+) *$';
rconParser.Configuration.Status.AddMapping(104, 6); // RConName
rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
@ -41,4 +41,4 @@ var plugin = {
onTickAsync: function (server) {
}
};
};

View File

@ -2,8 +2,8 @@ var rconParser;
var eventParser;
var plugin = {
author: 'fed',
version: 0.1,
author: 'fed, diamante',
version: 0.2,
name: 'H1-Mod Parser',
isParser: true,
@ -29,6 +29,7 @@ var plugin = {
rconParser.Configuration.DefaultRConPort = 27016;
eventParser.Configuration.GameDirectory = '';
eventParser.Configuration.LocalizeText = '\x1f';
rconParser.Version = 'H1 MP 1.15 build 1251288 Tue Jul 23 13:38:30 2019 win64';
rconParser.GameName = 11; // H1

View File

@ -2,8 +2,8 @@ var rconParser;
var eventParser;
var plugin = {
author: 'Xerxes, RaidMax, st0rm',
version: 0.4,
author: 'Xerxes, RaidMax, st0rm, Future',
version: 0.5,
name: 'IW6x Parser',
isParser: true,
@ -20,11 +20,12 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)?(.*)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.WaitForResponse = false;
rconParser.Configuration.DefaultRConPort = 28960;
rconParser.Configuration.Status.AddMapping(102, 4);
rconParser.Configuration.Status.AddMapping(103, 5);
rconParser.Configuration.Status.AddMapping(104, 6);
@ -33,7 +34,9 @@ var plugin = {
rconParser.GameName = 4; // IW6
eventParser.Version = 'IW6 MP 3.15 build 2 Sat Sep 14 2013 03:58:30PM win64';
eventParser.GameName = 4; // IW6
eventParser.Configuration.GameDirectory = '';
eventParser.Configuration.LocalizeText = '\x1f';
},
onUnloadAsync: function () {

View File

@ -2,8 +2,8 @@
var eventParser;
var plugin = {
author: 'Diavolo, RaidMax',
version: 0.2,
author: 'Future, RaidMax',
version: 0.4,
name: 'S1x Parser',
isParser: true,
@ -17,7 +17,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)?(.*)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.Status.AddMapping(102, 4);
@ -27,6 +27,7 @@ var plugin = {
rconParser.Configuration.DefaultRConPort = 27016;
eventParser.Configuration.GameDirectory = '';
eventParser.Configuration.LocalizeText = '\x1f';
rconParser.Version = 'S1 MP 1.22 build 2195988 Wed Apr 18 11:26:14 2018 win64';
rconParser.GameName = 9; // SHG1

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.4,
version: 0.5,
name: 'Black Ops 3 Parser',
isParser: true,
@ -15,7 +15,7 @@ var plugin = {
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown)(?:\\([0-9]+\\)) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport|---------- Live ----------';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0}';
@ -30,6 +30,7 @@ var plugin = {
rconParser.Configuration.DefaultRConPort = 27016;
rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'live_steam_server_name');
rconParser.Configuration.OverrideDvarNameMapping.Add('g_password', 'live_steam_server_password');
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.DefaultDvarValues.Add('g_gametype', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', '');

View File

@ -1,5 +1,6 @@
const cidrRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
const validCIDR = input => cidrRegex.test(input);
const subnetBanlistKey = 'Webfront::Nav::Admin::SubnetBanlist';
let subnetList = [];
const commands = [{
@ -26,7 +27,36 @@ const commands = [{
gameEvent.Origin.Tell(`Added ${input} to subnet banlist`);
}
}];
},
{
name: 'unbansubnet',
description: 'unbans an IPv4 subnet',
alias: 'ubs',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [{
name: 'subnet in IPv4 CIDR notation',
required: true
}],
execute: (gameEvent) => {
const input = String(gameEvent.Data).trim();
if (!validCIDR(input)) {
gameEvent.Origin.Tell('Invalid CIDR input');
return;
}
if (!subnetList.includes(input)) {
gameEvent.Origin.Tell('Subnet is not banned');
return;
}
subnetList = subnetList.filter(item => item !== input);
_configHandler.SetValue('SubnetBanList', subnetList);
gameEvent.Origin.Tell(`Removed ${input} from subnet banlist`);
}
}];
convertIPtoLong = ip => {
let components = String(ip).match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
@ -66,7 +96,7 @@ isSubnetBanned = (ip, list) => {
const plugin = {
author: 'RaidMax',
version: 1.0,
version: 1.1,
name: 'Subnet Banlist Plugin',
manager: null,
logger: null,
@ -86,7 +116,8 @@ const plugin = {
this.manager = manager;
this.logger = manager.GetLogger(0);
this.configHandler = _configHandler;
this.subnetList = [];
subnetList = [];
this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration');
const list = this.configHandler.GetValue('SubnetBanList');
if (list !== undefined) {
@ -105,9 +136,58 @@ const plugin = {
this.banMessage = 'You are not allowed to join this server.';
this.configHandler.SetValue('BanMessage', this.banMessage);
}
this.interactionRegistration.RegisterScriptInteraction(subnetBanlistKey, plugin.name, (targetId, game, token) => {
const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();
interactionData.Name = 'Subnet Banlist'; // navigation link name
interactionData.Description = `List of banned subnets (${subnetList.length} Total)`; // alt and title
interactionData.DisplayMeta = 'oi-circle-x'; // nav icon
interactionData.InteractionId = subnetBanlistKey;
interactionData.MinimumPermission = 3; // moderator
interactionData.InteractionType = 2; // 1 is RawContent for apis etc..., 2 is
interactionData.Source = plugin.name;
interactionData.ScriptAction = (sourceId, targetId, game, meta, token) => {
let table = '<table class="table bg-dark-dm bg-light-lm">';
const unbanSubnetInteraction = {
InteractionId: 'command',
Data: 'unbansubnet',
ActionButtonLabel: 'Unban',
Name: 'Unban Subnet'
};
subnetList.forEach(subnet => {
unbanSubnetInteraction.Data += ' ' + subnet
table += `<tr>
<td>
<p>${subnet}</p>
</td>
<td>
<a href="#" class="profile-action no-decoration float-right" data-action="DynamicAction"
data-action-meta="${encodeURI(JSON.stringify(unbanSubnetInteraction))}">
<div class="btn">
<i class="oi oi-circle-x mr-5 font-size-12"></i>
<span class="text-truncate">Unban Subnet</span>
</div>
</a>
</td>
</tr>`;
});
table += '</table>';
return table;
}
return interactionData;
});
},
onUnloadAsync: () => {
this.interactionRegistration.UnregisterInteraction(subnetBanlistKey);
},
onTickAsync: server => {

View File

@ -1,12 +1,15 @@
let vpnExceptionIds = [];
const vpnAllowListKey = 'Webfront::Nav::Admin::VPNAllowList';
const vpnWhitelistKey = 'Webfront::Profile::VPNWhitelist';
const commands = [{
name: "whitelistvpn",
description: "whitelists a player's client id from VPN detection",
alias: "wv",
permission: "SeniorAdmin",
name: 'whitelistvpn',
description: 'whitelists a player\'s client id from VPN detection',
alias: 'wv',
permission: 'SeniorAdmin',
targetRequired: true,
arguments: [{
name: "player",
name: 'player',
required: true
}],
execute: (gameEvent) => {
@ -15,21 +18,47 @@ const commands = [{
gameEvent.Origin.Tell(`Successfully whitelisted ${gameEvent.Target.Name}`);
}
},
{
name: 'disallowvpn',
description: 'disallows a player from connecting with a VPN',
alias: 'dv',
permission: 'SeniorAdmin',
targetRequired: true,
arguments: [{
name: 'player',
required: true
}],
execute: (gameEvent) => {
vpnExceptionIds = vpnExceptionIds.filter(exception => parseInt(exception) !== parseInt(gameEvent.Target.ClientId));
plugin.configHandler.SetValue('vpnExceptionIds', vpnExceptionIds);
gameEvent.Origin.Tell(`Successfully disallowed ${gameEvent.Target.Name} from connecting with VPN`);
}
}];
const getClientsData = (clientIds) => {
const contextFactory = _serviceResolver.ResolveService('IDatabaseContextFactory');
const context = contextFactory.CreateContext(false);
const clientSet = context.Clients;
const clients = clientSet.GetClientsBasicData(clientIds);
context.Dispose();
return clients;
}
const plugin = {
author: 'RaidMax',
version: 1.3,
version: 1.5,
name: 'VPN Detection Plugin',
manager: null,
logger: null,
checkForVpn: function (origin) {
let exempt = false;
// prevent players that are exempt from being kicked
vpnExceptionIds.forEach(function (id) {
if (id == origin.ClientId) { // when loaded from the config the "id" type is not the same as the ClientId type
if (parseInt(id) === parseInt(origin.ClientId)) {
exempt = true;
return false;
}
@ -80,11 +109,103 @@ const plugin = {
this.logger = manager.GetLogger(0);
this.configHandler = _configHandler;
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element));
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(parseInt(element)));
this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`);
this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration');
// registers the profile action
this.interactionRegistration.RegisterScriptInteraction(vpnWhitelistKey, this.name, (targetId, game, token) => {
const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();
interactionData.ActionPath = 'DynamicAction';
interactionData.InteractionId = vpnWhitelistKey;
interactionData.EntityId = targetId;
interactionData.MinimumPermission = 3;
interactionData.Source = this.name;
interactionData.ActionMeta.Add('InteractionId', 'command'); // indicate we're wanting to execute a command
interactionData.ActionMeta.Add('ShouldRefresh', true.toString()); // indicates that the page should refresh after performing the action
if (vpnExceptionIds.includes(targetId)) {
interactionData.Name = _localization.LocalizationIndex['WEBFRONT_VPN_BUTTON_DISALLOW']; // text for the profile button
interactionData.DisplayMeta = 'oi-circle-x';
interactionData.ActionMeta.Add('Data', `disallowvpn`); // command to execute
interactionData.ActionMeta.Add('ActionButtonLabel', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_CONFIRM']); // confirm button on the dialog
interactionData.ActionMeta.Add('Name', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_TITLE']); // title on the confirm dialog
} else {
interactionData.Name = _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_ALLOW']; // text for the profile button
interactionData.DisplayMeta = 'oi-circle-check';
interactionData.ActionMeta.Add('Data', `whitelistvpn`); // command to execute
interactionData.ActionMeta.Add('ActionButtonLabel', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_ALLOW_CONFIRM']); // confirm button on the dialog
interactionData.ActionMeta.Add('Name', _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_ALLOW_TITLE']); // title on the confirm dialog
}
return interactionData;
});
// registers the navigation/page
this.interactionRegistration.RegisterScriptInteraction(vpnAllowListKey, this.name, (targetId, game, token) => {
const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();
interactionData.Name = _localization.LocalizationIndex['WEBFRONT_NAV_VPN_TITLE']; // navigation link name
interactionData.Description = _localization.LocalizationIndex['WEBFRONT_NAV_VPN_DESC']; // alt and title
interactionData.DisplayMeta = 'oi-circle-check'; // nav icon
interactionData.InteractionId = vpnAllowListKey;
interactionData.MinimumPermission = 3; // moderator
interactionData.InteractionType = 2; // 1 is RawContent for apis etc..., 2 is
interactionData.Source = this.name;
interactionData.ScriptAction = (sourceId, targetId, game, meta, token) => {
const clientsData = getClientsData(vpnExceptionIds);
let table = '<table class="table bg-dark-dm bg-light-lm">';
const disallowInteraction = {
InteractionId: 'command',
Data: 'disallowvpn',
ActionButtonLabel: _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_CONFIRM'],
Name: _localization.LocalizationIndex['WEBFRONT_VPN_ACTION_DISALLOW_TITLE']
};
if (clientsData.length === 0)
{
table += `<tr><td>No players are whitelisted.</td></tr>`
}
clientsData.forEach(client => {
table += `<tr>
<td>
<a href="/Client/Profile/${client.ClientId}" class="level-color-${client.Level.toLowerCase()} no-decoration">${client.CurrentAlias.Name.StripColors()}</a>
</td>
<td>
<a href="#" class="profile-action no-decoration float-right" data-action="DynamicAction" data-action-id="${client.ClientId}"
data-action-meta="${encodeURI(JSON.stringify(disallowInteraction))}">
<div class="btn">
<i class="oi oi-circle-x mr-5 font-size-12"></i>
<span class="text-truncate">${_localization.LocalizationIndex['WEBFRONT_VPN_BUTTON_DISALLOW']}</span>
</div>
</a>
</td>
</tr>`;
});
table += '</table>';
return table;
}
return interactionData;
});
},
onUnloadAsync: function () {
this.interactionRegistration.UnregisterInteraction(vpnWhitelistKey);
this.interactionRegistration.UnregisterInteraction(vpnAllowListKey);
},
onTickAsync: function (server) {

View File

@ -856,7 +856,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{
#pragma warning disable CS0612
var serverDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
#pragma warning restore CS0612
var gameDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.GameDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -19,6 +19,7 @@ namespace SharedLibraryCore
{
public class BaseController : Controller
{
protected readonly IInteractionRegistration InteractionRegistration;
protected readonly IAlertManager AlertManager;
/// <summary>
@ -41,6 +42,7 @@ namespace SharedLibraryCore
public BaseController(IManager manager)
{
InteractionRegistration = manager.InteractionRegistration;
AlertManager = manager.AlertManager;
Manager = manager;
Localization = Utilities.CurrentLocalization.LocalizationIndex;
@ -67,13 +69,11 @@ namespace SharedLibraryCore
Client = new EFClient
{
ClientId = -1,
Level = Data.Models.Client.EFClient.Permission.Banned,
Level = Data.Models.Client.EFClient.Permission.User,
CurrentAlias = new EFAlias { Name = "Webfront Guest" }
};
}
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrinciple,
@ -86,7 +86,7 @@ namespace SharedLibraryCore
});
}
public override void OnActionExecuting(ActionExecutingContext context)
public override async void OnActionExecuting(ActionExecutingContext context)
{
if (!HttpContext.Connection.RemoteIpAddress.GetAddressBytes().SequenceEqual(LocalHost))
{
@ -154,6 +154,7 @@ namespace SharedLibraryCore
&& !communityName.Contains("IW4MAdmin")
&& AppConfig.CommunityInformation.IsEnabled;
ViewBag.Interactions = await InteractionRegistration.GetInteractions("Webfront::Nav");
ViewBag.Authorized = Authorized;
ViewBag.Url = AppConfig.WebfrontUrl;
ViewBag.User = Client;

View File

@ -15,7 +15,7 @@ namespace SharedLibraryCore.Dtos
/// <summary>
/// how many items to take
/// </summary>
public int Count { get; set; } = 100;
public int Count { get; set; } = 30;
/// <summary>
/// filter query
@ -28,6 +28,8 @@ namespace SharedLibraryCore.Dtos
public SortDirection Direction { get; set; } = SortDirection.Descending;
public DateTime? Before { get; set; }
public DateTime? After { get; set; }
}
public enum SortDirection

View File

@ -7,6 +7,7 @@ namespace SharedLibraryCore.Dtos
public class PenaltyInfo : SharedInfo
{
public string OffenderName { get; set; }
public Permission OffenderLevel { get; set; }
public int OffenderId { get; set; }
public ulong OffenderNetworkId { get; set; }
public string OffenderIPAddress { get; set; }
@ -38,4 +39,4 @@ namespace SharedLibraryCore.Dtos
public string AdditionalPenaltyInformation =>
$"{(!string.IsNullOrEmpty(AutomatedOffense) ? $" ({AutomatedOffense})" : "")}{(IsEvade ? $" ({Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PENALTY_EVADE"]})" : "")}";
}
}
}

View File

@ -34,5 +34,6 @@ namespace SharedLibraryCore.Dtos
public string CurrentServerName { get; set; }
public IGeoLocationResult GeoLocationInfo { get; set; }
public ClientNoteMetaResponse NoteMeta { get; set; }
public List<IInteractionData> Interactions { get; set; }
}
}

View File

@ -4,8 +4,15 @@ namespace SharedLibraryCore.Exceptions
{
public class RConException : Exception
{
public bool IsOperationCancelled { get; }
public RConException(string message) : base(message)
{
}
public RConException(string message, bool isOperationCancelled) : base(message)
{
IsOperationCancelled = isOperationCancelled;
}
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client;
using SharedLibraryCore.Interfaces;
using InteractionCallback = System.Func<int, int?, Data.Models.Reference.Game?, System.Collections.Generic.IDictionary<string,string>, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
namespace SharedLibraryCore.Helpers;
public class InteractionData : IInteractionData
{
public int? EntityId { get; set; }
public string InteractionId { get; set; }
public InteractionType InteractionType { get; set; }
public bool Enabled { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string DisplayMeta { get; set; }
public string ActionPath { get; set; }
public Dictionary<string, string> ActionMeta { get; set; } = new();
public string ActionUri => ActionPath + "?" + string.Join('&', ActionMeta.Select(kvp => $"{kvp.Key}={kvp.Value}"));
public EFClient.Permission? MinimumPermission { get; set; }
public string PermissionEntity { get; set; } = "Interaction";
public string PermissionAccess { get; set; } = "Read";
public string Source { get; set; }
public InteractionCallback Action { get; set; }
public Delegate ScriptAction { get; set; }
}

View File

@ -16,6 +16,11 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
ParserRegex Say { get; set; }
/// <summary>
/// stores the special ASCII value used by CoD games that prevents the text in the chat from being localized
/// </summary>
string LocalizeText { get; set; }
/// <summary>
/// stores the regex information for a join event printed in the game log
/// </summary>

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Database.Models;
@ -14,6 +15,8 @@ namespace SharedLibraryCore.Interfaces
/// <param name="origin">source of kick action</param>
/// <param name="previousPenalty">previous penalty the kick is occuring for (if applicable)</param>
/// <returns></returns>
public Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
DateTime? MatchEndTime { get; }
DateTime? MatchStartTime { get; }
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using Data.Models.Client;
using InteractionCallback = System.Func<int, int?, Data.Models.Reference.Game?, System.Collections.Generic.IDictionary<string,string>, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
namespace SharedLibraryCore.Interfaces;
public interface IInteractionData
{
int? EntityId { get; }
string InteractionId { get; }
InteractionType InteractionType { get; }
bool Enabled { get; }
string Name { get; }
string Description { get; }
string DisplayMeta { get; }
string ActionPath { get; }
Dictionary<string, string> ActionMeta { get; }
string ActionUri { get; }
EFClient.Permission? MinimumPermission { get; }
string PermissionEntity { get; }
string PermissionAccess { get; }
string Source { get; }
InteractionCallback Action { get; }
Delegate ScriptAction { get; }
}
public enum InteractionType
{
ActionButton,
RawContent,
TemplateContent
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
namespace SharedLibraryCore.Interfaces;
public interface IInteractionRegistration
{
void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration);
void RegisterInteraction(string interactionName, Func<int?, Reference.Game?, CancellationToken, Task<IInteractionData>> interactionRegistration);
void UnregisterInteraction(string interactionName);
Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null, int? clientId = null,
Reference.Game? game = null, CancellationToken token = default);
Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null, Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default);
}

View File

@ -23,6 +23,7 @@ namespace SharedLibraryCore.Interfaces
IList<IRConParser> AdditionalRConParsers { get; }
IList<IEventParser> AdditionalEventParsers { get; }
IMiddlewareActionHandler MiddlewareActionHandler { get; }
IList<Func<GameEvent, bool>> CommandInterceptors { get; }
string Version { get; }
ITokenAuthentication TokenAuthenticator { get; }
string ExternalIPAddress { get; }
@ -102,7 +103,8 @@ namespace SharedLibraryCore.Interfaces
/// event executed when event has finished executing
/// </summary>
event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
IInteractionRegistration InteractionRegistration { get; }
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Dtos;
namespace SharedLibraryCore.Interfaces;
public interface IRemoteCommandService
{
Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command, IEnumerable<string> arguments, Server server);
Task<(bool, IEnumerable<CommandResponseInfo>)> ExecuteWithResult(int originId, int? targetId, string command, IEnumerable<string> arguments, Server server);
}

View File

@ -100,6 +100,8 @@ namespace SharedLibraryCore
public List<ChatInfo> ChatHistory { get; protected set; }
public ClientHistoryInfo ClientHistory { get; }
public Game GameName { get; set; }
public DateTime? MatchEndTime { get; protected set; }
public DateTime? MatchStartTime { get; protected set; }
public string Hostname
{
@ -407,24 +409,9 @@ namespace SharedLibraryCore
public abstract Task<long> GetIdForServer(Server server = null);
public string[] ExecuteServerCommand(string command, int timeoutMs = 1000)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try
{
return this.ExecuteCommandAsync(command, tokenSource.Token).GetAwaiter().GetResult();
}
catch
{
return null;
}
}
public string GetServerDvar(string dvarName, int timeoutMs = 1000)
{
var tokenSource = new CancellationTokenSource();
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try
{
@ -438,7 +425,7 @@ namespace SharedLibraryCore
public bool SetServerDvar(string dvarName, string dvarValue, int timeoutMs = 1000)
{
var tokenSource = new CancellationTokenSource();
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
try
{

View File

@ -286,7 +286,10 @@ namespace SharedLibraryCore.Services
entity.PasswordSalt = temporalClient.PasswordSalt;
}
entity.GameName = temporalClient.GameName;
if (entity.GameName == Reference.Game.UKN && temporalClient.GameName != entity.GameName)
{
entity.GameName = temporalClient.GameName;
}
// update in database
await context.SaveChangesAsync();

View File

@ -117,6 +117,7 @@ namespace SharedLibraryCore.Services
AutomatedOffense = _penalty.AutomatedOffense,
OffenderId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
OffenderLevel = _penalty.Offender.Level,
PunisherId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PunisherLevel = _penalty.Punisher.Level,
@ -130,7 +131,7 @@ namespace SharedLibraryCore.Services
}
private static readonly EFPenalty.PenaltyType[] LinkedPenalties =
{ EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag, EFPenalty.PenaltyType.TempBan };
{ EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag, EFPenalty.PenaltyType.TempBan, EFPenalty.PenaltyType.TempMute, EFPenalty.PenaltyType.Mute };
private static readonly Expression<Func<EFPenalty, bool>> Filter = p =>
LinkedPenalties.Contains(p.Type) && p.Active && (p.Expires == null || p.Expires > DateTime.UtcNow);

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.6.16.1</Version>
<Version>2022.10.13.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.6.16.1</PackageVersion>
<PackageVersion>2022.10.13.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
@ -34,23 +34,23 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="10.3.6" />
<PackageReference Include="Humanizer.Core" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.ru" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.de" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.es" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.pt" Version="2.13.14" />
<PackageReference Include="FluentValidation" Version="11.2.1" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.de" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.es" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.pt" Version="2.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>

View File

@ -18,7 +18,6 @@ using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta;
using SharedLibraryCore.Formatting;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Localization;
@ -27,6 +26,7 @@ using static SharedLibraryCore.Server;
using static Data.Models.Client.EFClient;
using static Data.Models.EFPenalty;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using RegionInfo = System.Globalization.RegionInfo;
namespace SharedLibraryCore
{
@ -317,14 +317,20 @@ namespace SharedLibraryCore
}
}
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null)
{
return ConvertGuidToLong(str, numberStyle, true, fallback);
}
/// <summary>
/// converts a string to numerical guid
/// </summary>
/// <param name="str">source string for guid</param>
/// <param name="numberStyle">how to parse the guid</param>
/// <param name="fallback">value to use if string is empty</param>
/// <param name="convertSigned">convert signed values to unsigned</param>
/// <returns></returns>
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null)
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, bool convertSigned, long? fallback = null)
{
// added for source games that provide the steam ID
var match = Regex.Match(str, @"^STEAM_(\d):(\d):(\d+)$");
@ -337,7 +343,7 @@ namespace SharedLibraryCore
return z * 2 + 0x0110000100000000 + y;
}
str = str.Substring(0, Math.Min(str.Length, 19));
str = str.Substring(0, Math.Min(str.Length, str.StartsWith("-") ? 20 : 19));
var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value;
if (string.IsNullOrWhiteSpace(str) && fallback.HasValue)
@ -352,7 +358,7 @@ namespace SharedLibraryCore
{
long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out id);
if (id < 0)
if (id < 0 && convertSigned)
{
id = (uint)id;
}
@ -1003,9 +1009,9 @@ namespace SharedLibraryCore
return true;
}
catch (Exception e)
catch (Exception ex)
{
logger.LogError(e, $"Could not create penalty of type {penalty.Type.ToString()}");
logger.LogError(ex, "Could not create penalty of type {PenaltyType}", penalty.Type.ToString());
}
return false;
@ -1052,8 +1058,7 @@ namespace SharedLibraryCore
{
await Task.WhenAny(task, Task.Delay(timeout));
}
public static bool ShouldHideLevel(this Permission perm)
{
return perm == Permission.Flagged;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
@ -24,6 +25,9 @@ namespace WebfrontCore.Controllers
{
private readonly ApplicationConfiguration _appConfig;
private readonly IMetaServiceV2 _metaService;
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private readonly ITranslationLookup _translationLookup;
private readonly string _banCommandName;
private readonly string _tempbanCommandName;
private readonly string _unbanCommandName;
@ -37,10 +41,15 @@ namespace WebfrontCore.Controllers
private readonly string _addClientNoteCommandName;
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
ApplicationConfiguration appConfig, IMetaServiceV2 metaService) : base(manager)
ApplicationConfiguration appConfig, IMetaServiceV2 metaService,
IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService,
ITranslationLookup translationLookup) : base(manager)
{
_appConfig = appConfig;
_metaService = metaService;
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
_translationLookup = translationLookup;
foreach (var cmd in registeredCommands)
{
@ -86,6 +95,164 @@ namespace WebfrontCore.Controllers
}
}
public IActionResult DynamicActionForm(int? id, string meta)
{
if (Client.ClientId < 1)
{
return Ok(new[]
{
new CommandResponseInfo
{
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
});
}
if (meta is null)
{
return BadRequest(new[]
{
new CommandResponseInfo
{
Response = "INVALID"
}
});
}
var metaDict = JsonSerializer.Deserialize<Dictionary<string, string>>(meta.TrimEnd('"').TrimStart('"'));
if (metaDict is null)
{
return BadRequest();
}
metaDict.TryGetValue(nameof(ActionInfo.ActionButtonLabel), out var label);
metaDict.TryGetValue(nameof(ActionInfo.Name), out var name);
metaDict.TryGetValue(nameof(ActionInfo.ShouldRefresh), out var refresh);
metaDict.TryGetValue("Data", out var data);
metaDict.TryGetValue("InteractionId", out var interactionId);
metaDict.TryGetValue("Inputs", out var template);
List<InputInfo> additionalInputs = null;
var inputKeys = string.Empty;
if (!string.IsNullOrWhiteSpace(template))
{
additionalInputs = JsonSerializer.Deserialize<List<InputInfo>>(template);
}
if (additionalInputs is not null)
{
inputKeys = string.Join(",", additionalInputs.Select(input => input.Name));
}
bool.TryParse(refresh, out var shouldRefresh);
var inputs = new List<InputInfo>
{
new()
{
Name = "InteractionId",
Value = interactionId,
Type = "hidden"
},
new()
{
Name = "data",
Value = data,
Type = "hidden"
},
new()
{
Name = "TargetId",
Value = id?.ToString(),
Type = "hidden"
},
new()
{
Name = "CustomInputKeys",
Value = inputKeys,
Type = "hidden"
}
};
if (additionalInputs?.Any() ?? false)
{
inputs.AddRange(additionalInputs);
}
var info = new ActionInfo
{
ActionButtonLabel = label,
Name = name,
Action = nameof(DynamicActionAsync),
ShouldRefresh = shouldRefresh,
Inputs = inputs
};
return View("_ActionForm", info);
}
public async Task<IActionResult> DynamicActionAsync(CancellationToken token = default)
{
if (Client.ClientId < 1)
{
return Ok(new[]
{
new CommandResponseInfo
{
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
});
}
HttpContext.Request.Query.TryGetValue("InteractionId", out var interactionId);
HttpContext.Request.Query.TryGetValue("CustomInputKeys", out var inputKeys);
HttpContext.Request.Query.TryGetValue("Data", out var data);
HttpContext.Request.Query.TryGetValue("TargetId", out var targetIdString);
var inputs = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(inputKeys.ToString()))
{
foreach (var key in inputKeys.ToString().Split(","))
{
HttpContext.Request.Query.TryGetValue(key, out var input);
if (string.IsNullOrWhiteSpace(input))
{
continue;
}
inputs.Add(key, HttpContext.Request.Query[key]);
}
}
var game = (Reference.Game?)null;
var targetId = (int?)null;
if (int.TryParse(targetIdString.ToString().Split(",").Last(), out var parsedTargetId))
{
targetId = parsedTargetId;
}
if (targetId.HasValue)
{
game = (await Manager.GetClientService().Get(targetId.Value))?.GameName;
}
if (interactionId.ToString() != "command")
{
return Ok(await _interactionRegistration.ProcessInteraction(interactionId, Client.ClientId, targetId, game, inputs,
token));
}
var server = Manager.GetServers().First();
var (success, result) = await _remoteCommandService.ExecuteWithResult(Client.ClientId, targetId, data,
inputs.Values.Select(input => input), server);
return success ? Ok(result) : BadRequest(result);
}
public IActionResult BanForm()
{
var info = new ActionInfo
@ -292,7 +459,7 @@ namespace WebfrontCore.Controllers
{
ClientId = Client.ClientId
});
return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"],
state.Token,
$"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}",
@ -645,7 +812,7 @@ namespace WebfrontCore.Controllers
$"{_appConfig.CommandPrefix}{_setClientTagCommandName} @{targetId} {clientTag}"
}));
}
public async Task<IActionResult> AddClientNoteForm(int id)
{
var existingNote = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", id);
@ -682,7 +849,7 @@ namespace WebfrontCore.Controllers
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
@ -695,7 +862,6 @@ namespace WebfrontCore.Controllers
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
.Distinct()
.Select((value, _) => new
{
Value = value
@ -705,6 +871,7 @@ namespace WebfrontCore.Controllers
{
Value = ""
})
.Distinct()
.ToDictionary(item => item.Value, item => item.Value);
}
}

View File

@ -15,6 +15,7 @@ using Data.Models;
using SharedLibraryCore.Services;
using Stats.Config;
using WebfrontCore.Permissions;
using WebfrontCore.QueryHelpers.Models;
using WebfrontCore.ViewComponents;
namespace WebfrontCore.Controllers
@ -25,14 +26,20 @@ namespace WebfrontCore.Controllers
private readonly StatsConfiguration _config;
private readonly IGeoLocationService _geoLocationService;
private readonly ClientService _clientService;
private readonly IInteractionRegistration _interactionRegistration;
private readonly IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse> _clientResourceHelper;
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
IGeoLocationService geoLocationService, ClientService clientService) : base(manager)
IGeoLocationService geoLocationService, ClientService clientService,
IInteractionRegistration interactionRegistration,
IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse> clientResourceHelper) : base(manager)
{
_metaService = metaService;
_config = config;
_geoLocationService = geoLocationService;
_clientService = clientService;
_interactionRegistration = interactionRegistration;
_clientResourceHelper = clientResourceHelper;
}
[Obsolete]
@ -75,10 +82,14 @@ namespace WebfrontCore.Controllers
note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId);
}
var interactions =
await _interactionRegistration.GetInteractions("Webfront::Profile", id, client.GameName, token);
// even though we haven't set their level to "banned" yet
// (ie they haven't reconnected with the infringing player identifier)
// we want to show them as banned as to not confuse people.
if (activePenalties.Any(penalty => penalty.Type == EFPenalty.PenaltyType.Ban))
var hasActiveBan = activePenalties.Any(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (hasActiveBan)
{
client.Level = Data.Models.Client.EFClient.Permission.Banned;
}
@ -86,7 +97,9 @@ namespace WebfrontCore.Controllers
var displayLevelInt = (int)client.Level;
var displayLevel = client.Level.ToLocalizedLevelName();
if (!Authorized && client.Level.ShouldHideLevel())
// if a linked ban has been revoked but they haven't reconnected, we should not show them as still banned
var shouldHideBanLevel = !hasActiveBan && client.Level == Data.Models.Client.EFClient.Permission.Banned;
if (!Authorized && client.Level.ShouldHideLevel() || shouldHideBanLevel)
{
displayLevelInt = (int)Data.Models.Client.EFClient.Permission.User;
displayLevel = Data.Models.Client.EFClient.Permission.User.ToLocalizedLevelName();
@ -134,7 +147,8 @@ namespace WebfrontCore.Controllers
ingameClient.CurrentServer.Port),
CurrentServerName = ingameClient?.CurrentServer?.Hostname,
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString),
NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note
NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note,
Interactions = interactions.ToList()
};
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest
@ -153,7 +167,13 @@ namespace WebfrontCore.Controllers
});
}
clientDto.ActivePenalty = activePenalties.OrderByDescending(_penalty => _penalty.Type).FirstOrDefault();
// Reducing the enum value for Temp/Mute so bans appear in client banner first
clientDto.ActivePenalty = activePenalties.MaxBy(penalty => penalty.Type switch
{
EFPenalty.PenaltyType.TempMute => 0,
EFPenalty.PenaltyType.Mute => 1,
_ => (int)penalty.Type
});
clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive));
var strippedName = clientDto.Name.StripColors();
@ -226,6 +246,17 @@ namespace WebfrontCore.Controllers
return View("Find/Index", clientsDto);
}
public async Task<IActionResult> AdvancedFind(ClientResourceRequest request)
{
ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"];
ViewBag.ClientResourceRequest = request;
var response = await _clientResourceHelper.QueryResource(request);
return request.Offset > 0
? PartialView("Find/_AdvancedFindList", response.Results)
: View("Find/AdvancedFind", response.Results);
}
public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType,
CancellationToken token)
{

View File

@ -1,22 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
namespace WebfrontCore.Controllers
{
public class ConsoleController : BaseController
{
private readonly ApplicationConfiguration _appconfig;
private readonly IRemoteCommandService _remoteCommandService;
private readonly ITranslationLookup _translationLookup;
public ConsoleController(IManager manager) : base(manager)
public ConsoleController(IManager manager, IRemoteCommandService remoteCommandService, ITranslationLookup translationLookup) : base(manager)
{
_appconfig = manager.GetApplicationSettings().Configuration();
_remoteCommandService = remoteCommandService;
_translationLookup = translationLookup;
}
public IActionResult Index()
@ -25,6 +24,7 @@ namespace WebfrontCore.Controllers
{
Name = s.Hostname,
ID = s.EndPoint,
Game = (Data.Models.Reference.Game)s.GameName
});
ViewBag.Description = Localization["WEFBRONT_DESCRIPTION_CONSOLE"];
@ -36,76 +36,21 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> Execute(long serverId, string command)
{
var server = Manager.GetServers().First(s => s.EndPoint == serverId);
var client = new EFClient
if (Client.ClientId < 1)
{
ClientId = Client.ClientId,
Level = Client.Level,
NetworkId = Client.NetworkId,
CurrentServer = server,
CurrentAlias = new EFAlias()
{
Name = Client.Name
}
};
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
};
Manager.AddEvent(remoteEvent);
CommandResponseInfo[] response = null;
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 (System.OperationCanceledException)
{
response = new[]
return Ok(new[]
{
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
Response = _translationLookup["SERVER_COMMANDS_INTERCEPTED"]
}
};
});
}
return remoteEvent.Failed ? StatusCode(400, response) : Ok(response);
var server = Manager.GetServers().First(s => s.EndPoint == serverId);
var (success, response) = await _remoteCommandService.ExecuteWithResult(Client.ClientId, null, command,
Enumerable.Empty<string>(), server);
return success ? Ok(response) : StatusCode(400, response);
}
}
}

View File

@ -0,0 +1,37 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace WebfrontCore.Controllers;
public class InteractionController : BaseController
{
private readonly IInteractionRegistration _interactionRegistration;
public InteractionController(IManager manager, IInteractionRegistration interactionRegistration) : base(manager)
{
_interactionRegistration = interactionRegistration;
}
[HttpGet("[controller]/[action]/{interactionName}")]
public async Task<IActionResult> Render([FromRoute]string interactionName, CancellationToken token)
{
var interactionData = (await _interactionRegistration.GetInteractions(interactionName, token: token)).FirstOrDefault();
if (interactionData is null)
{
return NotFound();
}
ViewBag.Title = interactionData.Description;
var result = await _interactionRegistration.ProcessInteraction(interactionName, Client.ClientId, token: token);
return interactionData.InteractionType == InteractionType.TemplateContent
? View("Render", result ?? "")
: Ok(result);
}
}

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
@ -8,6 +10,7 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.AspNetCore.Authentication.Cookies;
using SharedLibraryCore.Commands;
using static SharedLibraryCore.GameEvent;
namespace WebfrontCore.Middleware
@ -18,68 +21,25 @@ namespace WebfrontCore.Middleware
internal class ClaimsPermissionRemoval
{
private readonly IManager _manager;
private readonly List<int> _privilegedClientIds;
private static readonly ConcurrentDictionary<int, (ClaimsState, DateTimeOffset?)> PrivilegedClientIds = new();
private readonly RequestDelegate _nextRequest;
private enum ClaimsState
{
Current,
Tainted
}
public ClaimsPermissionRemoval(RequestDelegate nextRequest, IManager manager)
{
_manager = manager;
_manager.OnGameEventExecuted += OnGameEvent;
_privilegedClientIds = new List<int>();
_nextRequest = nextRequest;
}
/// <summary>
/// Callback for the game event
/// </summary>
/// <param name="sender"></param>
/// <param name="gameEvent"></param>
private void OnGameEvent(object sender, GameEvent gameEvent)
{
if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
{
return;
}
lock (_privilegedClientIds)
{
switch (perm)
{
// we want to remove the claims when the client is demoted
case < EFClient.Permission.Trusted:
{
_privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId);
break;
}
// and add if promoted
case > EFClient.Permission.Trusted when !_privilegedClientIds.Contains(gameEvent.Target.ClientId):
{
_privilegedClientIds.Add(gameEvent.Target.ClientId);
break;
}
}
}
}
public async Task Invoke(HttpContext context)
{
// we want to load the initial list of privileged clients
bool hasAny;
lock (_privilegedClientIds)
{
hasAny = _privilegedClientIds.Any();
}
if (!hasAny)
{
var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(client => client.ClientId);
lock (_privilegedClientIds)
{
_privilegedClientIds.AddRange(ids);
}
}
await Initialize();
// sid stores the clientId
var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
@ -87,14 +47,16 @@ namespace WebfrontCore.Middleware
if (!string.IsNullOrEmpty(claimsId))
{
var clientId = int.Parse(claimsId);
bool hasKey;
lock (_privilegedClientIds)
bool isTainted;
bool hasPrivilege;
lock (PrivilegedClientIds)
{
hasKey = _privilegedClientIds.Contains(clientId);
hasPrivilege = PrivilegedClientIds.ContainsKey(clientId);
isTainted = hasPrivilege && PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted;
}
// they've been removed
if (!hasKey && clientId != 1)
if (!hasPrivilege || isTainted)
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
@ -102,5 +64,141 @@ namespace WebfrontCore.Middleware
await _nextRequest.Invoke(context);
}
private void OnGameEvent(object sender, GameEvent gameEvent)
{
if (gameEvent.Extra?.GetType() == typeof(SetPasswordCommand))
{
lock (PrivilegedClientIds)
{
PrivilegedClientIds[gameEvent.Origin.ClientId] = (ClaimsState.Tainted, DateTimeOffset.UtcNow);
}
return;
}
if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
{
return;
}
lock (PrivilegedClientIds)
{
switch (perm)
{
// we want to remove the claims when the client is demoted
case < EFClient.Permission.Trusted when PrivilegedClientIds.ContainsKey(gameEvent.Target.ClientId):
{
PrivilegedClientIds.Remove(gameEvent.Target.ClientId, out _);
break;
}
// and add if promoted
case > EFClient.Permission.Trusted:
{
if (!PrivilegedClientIds.ContainsKey(gameEvent.Target.ClientId))
{
PrivilegedClientIds.TryAdd(gameEvent.Target.ClientId, (ClaimsState.Current, null));
}
else
{
// they've been intra-moted, so we need to taint their claims
PrivilegedClientIds[gameEvent.Target.ClientId] =
(ClaimsState.Tainted, DateTimeOffset.UtcNow);
}
break;
}
}
}
}
private async Task Initialize()
{
// we want to load the initial list of privileged clients
bool hasAny;
lock (PrivilegedClientIds)
{
hasAny = PrivilegedClientIds.Any();
}
if (!hasAny)
{
var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(client => client.ClientId);
lock (PrivilegedClientIds)
{
foreach (var id in ids)
{
PrivilegedClientIds.TryAdd(id, (ClaimsState.Current, null));
}
}
}
}
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
if (context.Principal is null)
{
return;
}
var claimsId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
if (string.IsNullOrEmpty(claimsId))
{
return;
}
var clientId = int.Parse(claimsId);
bool shouldSignOut;
lock (PrivilegedClientIds)
{
// we want to log them out if they aren't in the privileged clients list
// or the token is tainted or the taint event occured after the token was generated
shouldSignOut = PrivilegedClientIds.ContainsKey(clientId) &&
(PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted ||
PrivilegedClientIds[clientId].Item2 is not null &&
PrivilegedClientIds[clientId].Item2.Value - context.Properties.IssuedUtc >
TimeSpan.FromSeconds(30));
}
if (shouldSignOut)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
public static Task OnSignedIn(CookieSignedInContext context)
{
if (context.Principal is null)
{
return Task.CompletedTask;
}
var claimsId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
if (string.IsNullOrEmpty(claimsId))
{
return Task.CompletedTask;
}
var clientId = int.Parse(claimsId);
lock (PrivilegedClientIds)
{
if (PrivilegedClientIds.ContainsKey(clientId))
{
PrivilegedClientIds[clientId] = PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted
? (ClaimsState.Current, DateTimeOffset.UtcNow)
: (ClaimsState.Current, null);
}
}
return Task.CompletedTask;
}
}
}

View File

@ -15,7 +15,9 @@ public enum WebfrontEntity
RecentPlayersPage,
ProfilePage,
AdminMenu,
ClientNote
ClientNote,
Interaction,
AdvancedSearch
}
public enum WebfrontPermission

View File

@ -0,0 +1,19 @@
using System;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore.QueryHelper;
namespace WebfrontCore.QueryHelpers.Models;
public class ClientResourceRequest : ClientPaginationRequest
{
public string ClientName { get; set; }
public bool IsExactClientName { get; set; }
public string ClientIp { get; set; }
public bool IsExactClientIp { get; set; }
public string ClientGuid { get; set; }
public DateTime? ClientConnected { get; set; }
public EFClient.Permission? ClientLevel { get; set; }
public Reference.Game? GameName { get; set; }
public bool IncludeGeolocationData { get; set; } = true;
}

View File

@ -0,0 +1,22 @@
using System;
using Data.Models;
using Data.Models.Client;
namespace WebfrontCore.QueryHelpers.Models;
public class ClientResourceResponse
{
public int ClientId { get; set; }
public int AliasId { get; set; }
public int LinkId { get; set; }
public string CurrentClientName { get; set; }
public string MatchedClientName { get; set; }
public int? CurrentClientIp { get; set; }
public int? MatchedClientIp { get; set; }
public string ClientCountryCode { get; set; }
public string ClientCountryDisplayName { get; set; }
public string ClientLevel { get; set; }
public EFClient.Permission ClientLevelValue { get; set; }
public DateTime LastConnection { get; set; }
public Reference.Game Game { get; set; }
}

View File

@ -105,6 +105,8 @@ namespace WebfrontCore
{
options.AccessDeniedPath = "/";
options.LoginPath = "/";
options.Events.OnValidatePrincipal += ClaimsPermissionRemoval.ValidateAsync;
options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn;
});
services.AddSingleton(Program.Manager);
@ -113,6 +115,9 @@ namespace WebfrontCore
services.AddSingleton<IResourceQueryHelper<FindClientRequest, FindClientResult>, ClientService>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, StatsInfoResult>, StatsResourceQueryHelper>();
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
services.AddScoped(sp =>
Program.ApplicationServiceProvider
.GetRequiredService<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>>());
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
// todo: this needs to be handled more gracefully
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<DefaultSettings>());
@ -138,6 +143,8 @@ namespace WebfrontCore
services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<StatsConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IInteractionRegistration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IRemoteCommandService>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Data.Models.Client;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using SharedLibraryCore;
@ -30,9 +31,9 @@ public class HasPermission : TagHelper
{
output.TagName = null;
var permissionLevel = _contextAccessor?.HttpContext?.User.Claims
.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value;
.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value ?? EFClient.Permission.User.ToString();
var hasPermission = permissionLevel != null && _permissionSets.ContainsKey(permissionLevel) &&
var hasPermission = _permissionSets.ContainsKey(permissionLevel) &&
_permissionSets[permissionLevel].HasPermission(Entity, Permission);
if (!hasPermission)
{

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace WebfrontCore.ViewModels
{

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