Compare commits

...

186 Commits

Author SHA1 Message Date
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
444c06e65e make sure color tokens are mapped for kick messages 2022-07-23 13:48:46 -05:00
561909158f improve penalty display on mobile view 2022-07-23 11:22:16 -05:00
cd12c3f26e set default permission for read message to user 2022-07-23 11:13:21 -05:00
c817f9a810 improve audit log display on mobile 2022-07-23 11:09:23 -05:00
b27ae1517e fix issue with duplicate key on top stats page 2022-07-22 10:28:26 -05:00
507688a175 small tweaks for notes/tags 2022-07-20 11:39:46 -05:00
d2cfd50e39 update webfront permission types 2022-07-20 10:34:33 -05:00
51e8b31e42 add client note command and feature 2022-07-20 10:32:26 -05:00
fa1567d3f5 add set client tag to webfront profile as button 2022-07-19 20:37:48 -05:00
f97e266c24 send correct type to inc/dec meta service in game interface 2022-07-16 17:47:07 -05:00
506b17dbb3 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-16 09:56:48 -05:00
bef8c08d90 misc performance graph display tweaks 2022-07-16 09:56:41 -05:00
b78c467539 tweaks and persistent guid update to game integration/interface 2022-07-16 09:32:07 -05:00
Edo
c3e042521a Improvements to game scripts (#253) 2022-07-16 08:40:10 -05:00
cb5f490d3b fix incorrect js bundle input source 2022-07-13 16:27:47 -05:00
0a55c54c42 update to game interface/integration for persistent stat data 2022-07-13 16:10:16 -05:00
f43f7b5040 misc webfront tweaks 2022-07-10 21:06:58 -05:00
540cf7489d update pluto t6 parser for unknown ip 2022-07-10 20:09:57 -05:00
1a72faee60 add date stamp to performance graphs / increase number of performance rating snapshots / localize graph timestamps 2022-07-10 17:06:46 -05:00
4e44bb5ea1 fix rcon issue on restart 2022-07-09 20:57:00 -05:00
9e17bcc38f improve ban management display and additional translations 2022-07-09 16:32:23 -05:00
4b33b33d01 fix issue with alert on warn in game interface 2022-07-09 14:23:08 -05:00
6f1bc7ab90 cleanup table display of admins on mobile display 2022-07-09 13:54:35 -05:00
63e1774cb6 gracefully handle when infoString does not include all expected data 2022-07-09 10:52:27 -05:00
61df873bb1 more localization tweaks 2022-07-08 20:40:27 -05:00
052eeb0615 fix tag on welcome issue 2022-07-08 20:39:58 -05:00
88e67747fe add option to normalize diacritics for rcon parsers (applied to T6) 2022-07-06 15:42:31 -05:00
5db94723aa Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-06 10:02:09 -05:00
ea8216ecdf Add H1 maps and gametypes (#252) 2022-07-06 10:01:01 -05:00
6abbcbe464 prevent waiting for response on quit command 2022-07-06 09:55:06 -05:00
57484690b6 clean up display and uniformity of social icons 2022-07-06 09:49:44 -05:00
7a022a1973 fix grouping of commands on help page 2022-07-05 15:57:39 -05:00
7108e23a03 fix issue with context menu close not working on mobile 2022-07-05 15:15:25 -05:00
77d25890da clean up some more translations 2022-07-05 12:42:17 -05:00
2fca68a7ea update webfront translation strings 2022-07-05 12:02:43 -05:00
a6c0a94f6c support per-command override of rcon timeouts / update t5 parser to reflect 2022-07-01 09:59:11 -05:00
71abaac9e1 remove reports on ban/tempban 2022-07-01 09:14:57 -05:00
e07651b931 fix toast message issue on pages with query params 2022-06-28 10:03:05 -05:00
5a2ee36df9 use "unknown" ip as bot indicator 2022-06-28 09:15:37 -05:00
2daa4991d1 fix issue with previous change 2022-06-21 16:57:06 -05:00
775c0a91b5 small parser changes 2022-06-21 16:33:11 -05:00
55bccc7d3d ensure commands are not displayed/usable for unsupported games 2022-06-17 13:11:44 -05:00
4322e8d882 add migration logic for MySQL case sensitivity 2022-06-17 09:44:14 -05:00
a92f9fc29c optimize client searching 2022-06-16 18:44:49 -05:00
fbf424c77d optimize chat filtering/searching 2022-06-16 18:03:23 -05:00
b8e001fcfe misc ui tweaks 2022-06-16 14:02:44 -05:00
5ab5b73ecf order report servers by most recent report 2022-06-16 10:11:01 -05:00
4534d24fe6 fix token auth issue 2022-06-16 10:07:03 -05:00
73c8d0da33 improve icon alignment for nav menu 2022-06-16 09:46:01 -05:00
16d75470b5 fix login persistence issue 2022-06-15 21:00:01 -05:00
f02552faa1 fix up query/check 2022-06-15 20:19:22 -05:00
a4923d03f9 hide token generation button for non-logged-in users 2022-06-15 19:39:53 -05:00
8ae6561f4e update schema to support unique guid + game combinations 2022-06-15 19:37:34 -05:00
deeb1dea87 set the rcon parser game name for retail WaW 2022-06-14 15:12:19 -05:00
9ab34614c5 don't publish disconnect event if no client id 2022-06-14 15:00:23 -05:00
2cff25d6b3 make alert menu scrollable for large # of alerts 2022-06-13 11:03:39 -05:00
df3e226dc9 actually fix the previous issue 2022-06-12 16:37:07 -05:00
ef3db63ba7 fix issue that shouldn't actually be an issue 2022-06-12 15:09:26 -05:00
49fe4520ff improve alert display for mobile 2022-06-12 12:20:08 -05:00
6587187a34 fix memory/database leak with ranked player count cache 2022-06-12 12:19:32 -05:00
b337e232a2 use bot ip address when determining if client is bot 2022-06-12 10:09:56 -05:00
a44b4e9475 add alert/notification functionality (for server connection events and messages) 2022-06-11 11:34:00 -05:00
ffb0e5cac1 update for t5 dvar format change 2022-06-11 09:56:28 -05:00
ecc2b5bf54 increase width of side context menu for longer server names 2022-06-09 13:59:00 -05:00
2ac9cc4379 fix bug with loading top stats for individual servers 2022-06-09 13:50:58 -05:00
215037095f remove extra parenthesis oops.. 2022-06-09 10:15:43 -05:00
5433d7d1d2 add total ranked client number for stats pages 2022-06-09 09:56:41 -05:00
0446fe1ec5 revert time out for status preventing server from entering unreachable state 2022-06-08 09:10:31 -05:00
cf2a00e5b3 add game to player profile and admins page 2022-06-07 21:58:32 -05:00
ab494a22cb add mwr to game list (h1) 2022-06-07 12:10:39 -05:00
b690579154 fix issue with meta event context after 1st page load 2022-06-05 16:35:39 -05:00
acc967e50a add ban management page 2022-06-05 16:27:56 -05:00
c493fbe13d add game badge to server overview 2022-06-04 09:58:30 -05:00
ee56a5db1f fix map/gametype alignment on server overview and add back ip display on connect click 2022-06-04 09:21:08 -05:00
f235d0fafd update for pluto t5 rcon issue 2022-06-03 17:01:58 -05:00
7ecf516278 add plutonium T5 parser. Must use ManualLogPath 2022-06-03 16:26:58 -05:00
210f1ca336 fix incorrect wildcard colorcode 2022-06-02 19:59:09 -05:00
a38789adb9 add default anticheat detection types 2022-06-02 18:30:22 -05:00
e459b2fcde Add per game anticheat configuration option for issue #203 2022-06-02 18:24:13 -05:00
26853a0005 fix issue with player name spacing on server overview at certain resolutions 2022-06-02 18:16:54 -05:00
ee14306db9 fix displaying correct server name on top players 2022-06-02 17:53:14 -05:00
169105e849 fix loader on mobile audit log view 2022-06-02 16:54:26 -05:00
7c10e0e3de add baninfo api 2022-06-02 16:48:47 -05:00
2f7eb07e39 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-06-02 15:51:59 -05:00
0f9f4f597b Create ParserH1MOD.js (#248)
Co-Authored-By: fed <58637860+fedddddd@users.noreply.github.com>

Co-authored-by: fed <58637860+fedddddd@users.noreply.github.com>
2022-06-02 09:25:29 -05:00
880f9333d9 Fixed formatting... Tabs/spaces 2022-06-02 09:25:00 -05:00
31da5d352e Broadcast bans (Anti-cheat and manuals) script plugin 2022-06-02 09:25:00 -05:00
83a469cae3 Fix !hide provide "mitigation" to noclip ghost bug 2022-06-02 09:25:00 -05:00
1f13f9122c fix intermittent issue with game interface during connection loss with servers 2022-06-01 11:25:11 -05:00
dd8c4f438f reduce logging for failed anticheat log parsing 2022-05-22 18:04:38 -05:00
2230036d45 fix issue with VPN banlist evaluation 2022-05-22 18:04:23 -05:00
1700b7da91 PlutoIW5 support for the Game Interface and improvements to the GSC part of it. (#242)
* Improvements to the GSC part of the Game Interface
* Adds compatibility with PlutoIW5 with minimal changes.
* Fixes issues when commands are called from the web interface when the used profile is not on the server.
    * New Debug output when the target or origin of a command is sent by IW4MAdmin but not found in-game.
    * Commands that can be run on the context of the target are now run in it.
* Simplifies the command registration and execution.
    * Got rid of the huge switch block.
    * Introduced AddClientCommand to register new commands for example
        * `AddClientCommand("SwitchTeams",  true,  ::TeamSwitchImpl);`
        * `AddClientCommand("Hide",         false, ::HideImpl);`
    * Callbacks are called with the full event object and the parsed data as parameters to allow maximum flexibility.
* Introduced level.eventBus.gamename to know which game we are to add minor changes.
* Changes - noclip/lockcontrols/playertome
Additional changes to support other games' functions

Co-Authored-By: Amos <4959320+MrAmos123@users.noreply.github.com>
2022-05-19 17:04:34 -05:00
fab97ccad4 fix suffixing commands with color code 2022-04-28 17:22:15 -05:00
0bf0d033f7 make social icons fit better 2022-04-28 17:22:01 -05:00
2bbabcb9e8 fix issue with side nav 2022-04-28 12:05:58 -05:00
1995dbd080 improve loading of recent clients 2022-04-28 11:42:23 -05:00
a3b94b50e3 reduce warning logs for connecting bots on live radar 2022-04-28 10:35:01 -05:00
bc38b36e4a ignore bots for game interface 2022-04-28 10:20:55 -05:00
e346aa037e don't use cancellation token when persisting meta on quit 2022-04-28 10:14:35 -05:00
389c687420 fix issue with kick from profile 2022-04-28 10:09:25 -05:00
074e36413e format all output for color keys 2022-04-27 15:36:58 -05:00
104d9bdc4c make recent clients pagination load 20 per request 2022-04-25 16:13:18 -05:00
c51d28937b max recent clients paginated 2022-04-25 16:12:25 -05:00
ffa8a46feb move action modal infront of context modal for mobile 2022-04-25 15:52:18 -05:00
91c46dbdd4 only show reports from the last 24 hours 2022-04-25 15:44:51 -05:00
ff0d22c142 fix rcon issue 2022-04-25 15:39:30 -05:00
3ad4aa2196 escape html characters in web console output 2022-04-25 10:43:16 -05:00
d462892467 add configuration link for console perms 2022-04-23 10:33:48 -05:00
cabedb6f0b fix live radar icon 2022-04-22 17:29:29 -05:00
5b7f5160b2 show login token for longer period 2022-04-22 16:56:29 -05:00
0a8e415af8 add game to client 2022-04-22 16:03:34 -05:00
7b3ddd58c6 clean up report dropdown 2022-04-22 15:14:23 -05:00
ed1032415e fix live radar links 2022-04-22 15:13:51 -05:00
35b43e7438 fix issue with penalty list 2022-04-22 08:04:01 -05:00
284c2e9726 ui tweaks/improvements including recent players and ip info lookup 2022-04-21 12:39:09 -05:00
fd049edb3f fix kick button margin 2022-04-20 14:46:15 -05:00
4884abee76 better align player names/chat 2022-04-20 14:16:34 -05:00
1df76b6ac3 add missing live radar view to source control 2022-04-20 13:14:17 -05:00
4c42a1d511 fix accent colors not showing 2022-04-20 13:12:42 -05:00
27635a6dd3 done 2022-04-20 10:57:00 -05:00
0175425708 fix subnet ban and vpn detection persistence 2022-04-20 10:45:30 -05:00
5e12bf60b5 finish pipeline edits 2022-04-20 10:32:39 -05:00
2f10ca8599 override the font library file name 2022-04-20 10:22:41 -05:00
62ec18309e testing script 2022-04-20 09:56:46 -05:00
87361bf3d7 cleanup 2022-04-20 09:26:10 -05:00
dc45136077 test replace script 2022-04-20 09:03:29 -05:00
21b0a7998d fixup icons font path 2022-04-20 08:39:16 -05:00
20b8f0b99a ok for real this time 2022-04-19 23:14:10 -05:00
314ff96e71 trying again 2022-04-19 22:44:36 -05:00
d7c4f5452c move bundle step after publish 2022-04-19 22:37:45 -05:00
3cb50635e5 small updates that got lost in last commit 2022-04-19 22:34:35 -05:00
4fbe0ee0ed huge commit for webfront facelift 2022-04-19 18:43:58 -05:00
4023ca37d4 Hide numerical prefix for about page rules if included in the config 2022-04-09 10:16:34 -05:00
425ec2621d strip color keys from webfront form lists 2022-04-08 17:14:04 -05:00
15c3ca53e2 fix edge case data collection for offline servers/clean up implementation 2022-04-08 16:41:44 -05:00
6097ca504c fix issues with infinite profile meta scrolling 2022-04-08 14:26:17 -05:00
70cd01eafb reduce logging for meta lookup 2022-04-06 14:08:00 -05:00
a2d5e37c6f simplify initial setup by removing extra prompts 2022-04-06 13:04:30 -05:00
19bd47d0f4 initial permissions based webfront access implementation 2022-04-04 22:16:40 -05:00
dc97956bc3 add searching by partial ip address 2022-04-04 14:27:22 -05:00
89fdc00f9b add subnet ban command 2022-03-30 22:15:29 -05:00
039a05d9ad fix game tab selection on home 2022-03-30 15:44:05 -05:00
25fb5fdc14 fix new client graph for smaller screen sizes 2022-03-29 18:59:27 -05:00
1e67f6e86c collect data when server offline 2022-03-29 17:18:41 -05:00
7dbdf87728 disable map change indicator temporarily 2022-03-29 16:59:32 -05:00
180a4911bc improve server clientcount/activity graph on server overview 2022-03-29 16:42:53 -05:00
31123d9a33 include css change for previous commit 2022-03-28 21:34:52 -05:00
3cf0f54ceb remove striped scoreboard and add spectator color 2022-03-28 18:23:11 -05:00
eafd7cb530 add join team and map change events to CSGO parser 2022-03-28 18:05:18 -05:00
770785e979 misc fix 2022-03-28 16:05:00 -05:00
92d713d188 tweak scoreboard zscore again 2022-03-25 13:39:51 -05:00
34e531ef8d mark no zscore as 0 for scoreboard 2022-03-25 13:18:24 -05:00
724992ef33 set team properly/tint scoreboard background for team 2022-03-25 13:16:41 -05:00
557cc1614f improve ban handling edge cases 2022-03-25 11:28:15 -05:00
f90cdbef16 fix meta filter on profile 2022-03-24 16:23:40 -05:00
a863f78678 only unload plugins once at shutdown
clean up some doc warnings
2022-03-24 11:34:32 -05:00
c93f896bc5 fix profile issue 2022-03-24 08:40:42 -05:00
ccc8316a2f fix minimap image for live radar 2022-03-24 08:40:20 -05:00
291 changed files with 45860 additions and 4534 deletions

1
.gitignore vendored
View File

@ -224,7 +224,6 @@ bootstrap-custom.min.css
bootstrap-custom.css bootstrap-custom.css
**/Master/static **/Master/static
**/Master/dev_env **/Master/dev_env
/WebfrontCore/Views/Plugins/*
/WebfrontCore/wwwroot/**/dds /WebfrontCore/wwwroot/**/dds
/WebfrontCore/wwwroot/images/radar/* /WebfrontCore/wwwroot/images/radar/*

View File

@ -0,0 +1,55 @@
using System;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.Alerts;
public static class AlertExtensions
{
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
{
return new Alert.AlertState
{
RecipientId = client.ClientId,
Category = type ?? Alert.AlertCategory.Information
};
}
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
{
state.Category = category;
return state;
}
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
{
state.Type = type;
return state;
}
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
{
state.Message = message;
return state;
}
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
{
state.ExpiresAt = DateTime.Now.Add(expiration);
return state;
}
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
{
state.Source = source;
return state;
}
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
{
state.Source = client.Name.StripColors();
state.SourceId = client.ClientId;
return state;
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Alerts;
public class AlertManager : IAlertManager
{
private readonly ApplicationConfiguration _appConfig;
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
public AlertManager(ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_states.TryAdd(0, new List<Alert.AlertState>());
}
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
public async Task Initialize()
{
foreach (var source in _staticSources)
{
var alerts = await source();
foreach (var alert in alerts)
{
AddAlert(alert);
}
}
}
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
{
lock (_states)
{
var alerts = Enumerable.Empty<Alert.AlertState>();
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
{
alerts = alerts.Concat(_states[0].Where(alert =>
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
}
if (_states.ContainsKey(client.ClientId))
{
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
}
public void MarkAlertAsRead(Guid alertId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
if (matchingEvent is null)
{
continue;
}
items.Remove(matchingEvent);
OnAlertConsumed?.Invoke(this, matchingEvent);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
{
if (item.RecipientId != null && item.RecipientId != recipientId)
{
return false;
}
OnAlertConsumed?.Invoke(this, item);
return true;
});
}
}
}
public void AddAlert(Alert.AlertState alert)
{
lock (_states)
{
if (alert.RecipientId is null)
{
_states[0].Add(alert);
return;
}
if (!_states.ContainsKey(alert.RecipientId.Value))
{
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
}
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
{
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
}
_states[alert.RecipientId.Value].Add(alert);
PruneOldAlerts();
}
}
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
{
_staticSources.Add(alertSource);
}
private void PruneOldAlerts()
{
foreach (var value in _states.Values)
{
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
}
}
}

View File

@ -24,13 +24,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2037" /> <PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1"> <PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.5" /> <PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup> </ItemGroup>
@ -63,6 +64,9 @@
<None Update="Configuration\LoggingConfiguration.json"> <None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Resources\GeoLite2-Country.mmdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View File

@ -46,6 +46,8 @@ namespace IW4MAdmin.Application
public IList<IRConParser> AdditionalRConParsers { get; } public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; } public IList<IEventParser> AdditionalEventParsers { get; }
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; } public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token; public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; } public string ExternalIPAddress { get; private set; }
@ -57,10 +59,11 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource; private CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>(); private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration; private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
@ -82,18 +85,19 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = clientService;
PenaltySvc = penaltyService; PenaltySvc = penaltyService;
_alertManager = alertManager;
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) }; AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
@ -508,6 +512,7 @@ namespace IW4MAdmin.Application
#endregion #endregion
_metaRegistration.Register(); _metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS #region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -587,16 +592,30 @@ namespace IW4MAdmin.Application
public async Task Start() => await UpdateServerStates(); public async Task Start() => await UpdateServerStates();
public void Stop() public async Task Stop()
{ {
foreach (var plugin in Plugins)
{
try
{
await plugin.OnUnloadAsync().WithTimeout(Utilities.DefaultCommandTimeout);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
}
}
_tokenSource.Cancel(); _tokenSource.Cancel();
IsRunning = false; IsRunning = false;
} }
public void Restart() public void Restart()
{ {
IsRestartRequested = true; IsRestartRequested = true;
Stop(); Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
} }
[Obsolete] [Obsolete]
@ -618,7 +637,7 @@ namespace IW4MAdmin.Application
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ? public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
GetActiveClients() GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client : .FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client; client;
public ClientService GetClientService() public ClientService GetClientService()
@ -684,5 +703,6 @@ namespace IW4MAdmin.Application
} }
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
} }
} }

View File

@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using IW4MAdmin.Application.Meta;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class AddClientNoteCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public AddClientNoteCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(config, layout)
{
Name = "addnote";
Description = _translationLookup["COMMANDS_ADD_CLIENT_NOTE_DESCRIPTION"];
Alias = "an";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_NOTE"],
Required = false
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var note = new ClientNoteMetaResponse
{
Note = gameEvent.Data?.Trim(),
OriginEntityId = gameEvent.Origin.ClientId,
ModifiedDate = DateTime.UtcNow
};
await _metaService.SetPersistentMetaValue("ClientNotes", note, gameEvent.Target.ClientId);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_NOTE_SUCCESS"]);
}
}

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Misc; using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,10 +19,12 @@ namespace IW4MAdmin.Application.Commands
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024; private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout, public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout) IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
: base(config, layout)
{ {
Name = "offlinemessage"; Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"]; Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
@ -29,6 +34,51 @@ namespace IW4MAdmin.Application.Commands
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_alertManager = alertManager;
_alertManager.RegisterStaticAlertSource(async () =>
{
var context = contextFactory.CreateContext(false);
return await context.InboxMessages.Where(message => !message.IsDelivered)
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
.Select(message => new Alert.AlertState
{
OccuredAt = message.CreatedDateTime,
Message = message.Message,
ExpiresAt = DateTime.UtcNow.AddDays(7),
Category = Alert.AlertCategory.Message,
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
SourceId = message.SourceClientId,
RecipientId = message.DestinationClientId,
ReferenceId = message.InboxMessageId,
Type = nameof(EFInboxMessage)
}).ToListAsync();
});
_alertManager.OnAlertConsumed += (_, state) =>
{
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
{
return;
}
try
{
var context = contextFactory.CreateContext(true);
foreach (var message in context.InboxMessages
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
{
message.IsDelivered = true;
}
context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
}
};
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
@ -47,14 +97,15 @@ namespace IW4MAdmin.Application.Commands
if (gameEvent.Target.IsIngame) if (gameEvent.Target.IsIngame)
{ {
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
.FormatExt(gameEvent.Target.Name));
return; return;
} }
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString()); var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage() var newMessage = new EFInboxMessage
{ {
SourceClientId = gameEvent.Origin.ClientId, SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId, DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
Message = gameEvent.Data, Message = gameEvent.Data,
}; };
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
.WithMessage(gameEvent.Data.Trim())
.FromClient(gameEvent.Origin)
.OfType(nameof(EFInboxMessage))
.ExpiresIn(TimeSpan.FromDays(7)));
try try
{ {
context.Set<EFInboxMessage>().Add(newMessage); context.Set<EFInboxMessage>().Add(newMessage);

View File

@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Commands
Name = "readmessage"; Name = "readmessage";
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"]; Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
Alias = "rm"; Alias = "rm";
Permission = EFClient.Permission.Flagged; Permission = EFClient.Permission.User;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;

View File

@ -564,6 +564,55 @@
"Alias": "Momentum" "Alias": "Momentum"
} }
] ]
},
{
"Game": "H1",
"Gametypes": [
{
"Name": "conf",
"Alias": "Kill Confirmed"
},
{
"Name": "ctf",
"Alias": "Capture The Flag"
},
{
"Name": "dd",
"Alias": "Demolition"
},
{
"Name": "dm",
"Alias": "Free For All"
},
{
"Name": "dom",
"Alias": "Domination"
},
{
"Name": "gun",
"Alias": "Gun Game"
},
{
"Name": "hp",
"Alias": "Hardpoint"
},
{
"Name": "koth",
"Alias": "Headquarters"
},
{
"Name": "sab",
"Alias": "Sabotage"
},
{
"Name": "sd",
"Alias": "Search & Destroy"
},
{
"Name": "war",
"Alias": "Team Deathmatch"
}
]
} }
], ],
"Maps": [ "Maps": [
@ -1768,6 +1817,103 @@
} }
] ]
}, },
{
"Game": "H1",
"Maps": [
{
"Alias": "Ambush",
"Name": "mp_convoy"
},
{
"Alias": "Backlot",
"Name": "mp_backlot"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Bog",
"Name": "mp_bog"
},
{
"Alias": "Countdown",
"Name": "mp_countdown"
},
{
"Alias": "Crash",
"Name": "mp_crash"
},
{
"Alias": "Crossfire",
"Name": "mp_crossfire"
},
{
"Alias": "District",
"Name": "mp_citystreets"
},
{
"Alias": "Downpour",
"Name": "mp_farm"
},
{
"Alias": "Overgrown",
"Name": "mp_overgrown"
},
{
"Alias": "Pipeline",
"Name": "mp_pipeline"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
},
{
"Alias": "Showdown",
"Name": "mp_showdown"
},
{
"Alias": "Strike",
"Name": "mp_strike"
},
{
"Alias": "Vacant",
"Name": "mp_vacant"
},
{
"Alias": "Wet Work",
"Name": "mp_cargoship"
},
{
"Alias": "Winter Crash",
"Name": "mp_crash_snow"
},
{
"Alias": "Broadcast",
"Name": "mp_broadcast"
},
{
"Alias": "Creek",
"Name": "mp_creek"
},
{
"Alias": "Chinatown",
"Name": "mp_carentan"
},
{
"Alias": "Killhouse",
"Name": "mp_killhouse"
},
{
"Alias": "Day Break",
"Name": "mp_farm_spring"
},
{
"Alias": "Beach Bog",
"Name": "mp_bog_summer"
}
]
},
{ {
"Game": "CSGO", "Game": "CSGO",
"Maps": [ "Maps": [

View File

@ -97,18 +97,21 @@ namespace IW4MAdmin.Application.EventParsers
{Configuration.Say, GameEvent.EventType.Say}, {Configuration.Say, GameEvent.EventType.Say},
{Configuration.Kill, GameEvent.EventType.Kill}, {Configuration.Kill, GameEvent.EventType.Kill},
{Configuration.MapChange, GameEvent.EventType.MapChange}, {Configuration.MapChange, GameEvent.EventType.MapChange},
{Configuration.MapEnd, GameEvent.EventType.MapEnd} {Configuration.MapEnd, GameEvent.EventType.MapEnd},
{Configuration.JoinTeam, GameEvent.EventType.JoinTeam}
}; };
_eventTypeMap = new Dictionary<string, GameEvent.EventType> _eventTypeMap = new Dictionary<string, GameEvent.EventType>
{ {
{"say", GameEvent.EventType.Say}, {"say", GameEvent.EventType.Say},
{"sayteam", GameEvent.EventType.Say}, {"sayteam", GameEvent.EventType.Say},
{"chat", GameEvent.EventType.Say},
{"chatteam", GameEvent.EventType.Say},
{"K", GameEvent.EventType.Kill}, {"K", GameEvent.EventType.Kill},
{"D", GameEvent.EventType.Damage}, {"D", GameEvent.EventType.Damage},
{"J", GameEvent.EventType.PreConnect}, {"J", GameEvent.EventType.PreConnect},
{"JT", GameEvent.EventType.JoinTeam}, {"JT", GameEvent.EventType.JoinTeam},
{"Q", GameEvent.EventType.PreDisconnect}, {"Q", GameEvent.EventType.PreDisconnect}
}; };
} }
@ -340,6 +343,11 @@ namespace IW4MAdmin.Application.EventParsers
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]]; var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]];
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]]; var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
if (Configuration.TeamMapping.ContainsKey(team))
{
team = Configuration.TeamMapping[team].ToString();
}
var networkId = originIdString.IsBotGuid() ? var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -359,7 +367,7 @@ namespace IW4MAdmin.Application.EventParsers
State = EFClient.ClientState.Connected, State = EFClient.ClientState.Connected,
}, },
Extra = team, Extra = team,
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime, GameTime = gameTime,
Source = GameEvent.EventSource.Log Source = GameEvent.EventSource.Log
}; };

View File

@ -1,6 +1,8 @@
using SharedLibraryCore.Interfaces; using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
using System.Globalization; using System.Globalization;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -23,6 +25,8 @@ namespace IW4MAdmin.Application.EventParsers
public ParserRegex MapEnd { get; set; } public ParserRegex MapEnd { get; set; }
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public Dictionary<string, EFClient.TeamType> TeamMapping { get; set; } = new();
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {
Say = parserRegexFactory.CreateParserRegex(); Say = parserRegexFactory.CreateParserRegex();

View File

@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -24,8 +25,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models; using Data.Models;
using Data.Models.Server; using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands; using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
namespace IW4MAdmin namespace IW4MAdmin
@ -73,7 +76,7 @@ namespace IW4MAdmin
{ {
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber); ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId); var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
// first time client is connecting to server // first time client is connecting to server
if (client == null) if (client == null)
@ -96,6 +99,8 @@ namespace IW4MAdmin
client.ClientNumber = clientFromLog.ClientNumber; client.ClientNumber = clientFromLog.ClientNumber;
client.Score = clientFromLog.Score; client.Score = clientFromLog.Score;
client.Ping = clientFromLog.Ping; client.Ping = clientFromLog.Ping;
client.Team = clientFromLog.Team;
client.TeamName = clientFromLog.TeamName;
client.CurrentServer = this; client.CurrentServer = this;
client.State = ClientState.Connecting; client.State = ClientState.Connecting;
@ -114,7 +119,7 @@ namespace IW4MAdmin
public override async Task OnClientDisconnected(EFClient client) public override async Task OnClientDisconnected(EFClient client)
{ {
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId)) if (GetClientsAsList().All(eachClient => eachClient.NetworkId != client.NetworkId))
{ {
using (LogContext.PushProperty("Server", ToString())) using (LogContext.PushProperty("Server", ToString()))
{ {
@ -150,11 +155,9 @@ namespace IW4MAdmin
{ {
if (E.IsBlocking) if (E.IsBlocking)
{ {
await E.Origin?.Lock(); await E.Origin.Lock();
} }
bool canExecuteCommand = true;
try try
{ {
if (!await ProcessEvent(E)) if (!await ProcessEvent(E))
@ -162,53 +165,52 @@ namespace IW4MAdmin
return; return;
} }
Command C = null; Command command = null;
if (E.Type == GameEvent.EventType.Command) if (E.Type == GameEvent.EventType.Command)
{ {
try try
{ {
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration); command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
} }
catch (CommandException e) catch (CommandException e)
{ {
ServerLogger.LogWarning(e, "Error validating command from event {@event}", ServerLogger.LogWarning(e, "Error validating command from event {@Event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId }); new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid; E.FailReason = GameEvent.EventFailReason.Invalid;
} }
if (C != null) if (command != null)
{ {
E.Extra = C; E.Extra = command;
} }
} }
var canExecuteCommand = Manager.CommandInterceptors.All(interceptor =>
{
try try
{ {
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login"); return interceptor(E);
if (loginPlugin != null)
{
await loginPlugin.OnEventAsync(E, this);
} }
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}"); ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
canExecuteCommand = false; E.Origin.ToString());
} await cmd.ExecuteAsync(E);
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
await command.ExecuteAsync(E);
} }
var pluginTasks = Manager.Plugins var pluginTasks = Manager.Plugins
.Where(_plugin => _plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E)); .Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks); await Task.WhenAll(pluginTasks);
@ -304,6 +306,14 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Error)
.FromSource("System")
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
} }
Throttled = true; Throttled = true;
@ -316,7 +326,15 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Information)
.FromSource("System")
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
} }
if (!string.IsNullOrEmpty(CustomSayName)) if (!string.IsNullOrEmpty(CustomSayName))
@ -353,9 +371,9 @@ namespace IW4MAdmin
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken); EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken);
if (clientTag?.LinkedMeta != null) if (clientTag?.Value != null)
{ {
E.Origin.Tag = clientTag.LinkedMeta.Value; E.Origin.Tag = clientTag.Value;
} }
try try
@ -429,6 +447,7 @@ namespace IW4MAdmin
Clients[E.Origin.ClientNumber] = E.Origin; Clients[E.Origin.ClientNumber] = E.Origin;
try try
{ {
E.Origin.GameName = (Reference.Game)GameName;
E.Origin = await OnClientConnected(E.Origin); E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin; E.Target = E.Origin;
} }
@ -496,7 +515,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin); E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId, await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.CurrentAlias?.IPAddress); E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty); await Manager.GetPenaltyService().Create(unflagPenalty);
} }
@ -506,7 +525,8 @@ namespace IW4MAdmin
{ {
Origin = E.Origin, Origin = E.Origin,
Target = E.Target, Target = E.Target,
Reason = E.Data Reason = E.Data,
ReportedOn = DateTime.UtcNow
}); });
var newReport = new EFPenalty() var newReport = new EFPenalty()
@ -569,10 +589,8 @@ namespace IW4MAdmin
Time = DateTime.UtcNow Time = DateTime.UtcNow
}); });
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId, await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId);
Manager.CancellationToken); await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId);
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId,
Manager.CancellationToken);
} }
else if (E.Type == GameEvent.EventType.PreDisconnect) else if (E.Type == GameEvent.EventType.PreDisconnect)
@ -643,7 +661,7 @@ namespace IW4MAdmin
} }
} }
ChatHistory.Add(new ChatInfo() ChatHistory.Add(new ChatInfo
{ {
Name = E.Origin.Name, Name = E.Origin.Name,
Message = message, Message = message,
@ -668,24 +686,51 @@ namespace IW4MAdmin
} }
else else
{
if (dict.ContainsKey("gametype"))
{ {
Gametype = dict["gametype"]; Gametype = dict["gametype"];
Hostname = dict["hostname"]; }
string mapname = dict["mapname"] ?? CurrentMap.Name; if (dict.ContainsKey("hostname"))
UpdateMap(mapname); {
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
? dict["mapname"] ?? CurrentMap.Name
: CurrentMap.Name;
UpdateMap(newMapName);
} }
} }
else else
{ {
var dict = (Dictionary<string, string>)E.Extra; var dict = (Dictionary<string, string>)E.Extra;
if (dict.ContainsKey("g_gametype"))
{
Gametype = dict["g_gametype"]; Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"]; }
MaxClients = int.Parse(dict["sv_maxclients"]);
string mapname = dict["mapname"]; if (dict.ContainsKey("sv_hostname"))
UpdateMap(mapname); {
Hostname = dict["sv_hostname"];
}
if (dict.ContainsKey("sv_maxclients"))
{
MaxClients = int.Parse(dict["sv_maxclients"]);
}
else if (dict.ContainsKey("com_maxclients"))
{
MaxClients = int.Parse(dict["com_maxclients"]);
}
if (dict.ContainsKey("mapname"))
{
UpdateMap(dict["mapname"]);
}
} }
if (E.GameTime.HasValue) if (E.GameTime.HasValue)
@ -722,6 +767,34 @@ namespace IW4MAdmin
E.Origin.UpdateTeam(E.Extra as string); E.Origin.UpdateTeam(E.Extra as string);
} }
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentClientGuid")
{
var parts = E.Data.Split(",");
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
int.TryParse(parts[1], out var low))
{
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
var penalties = await Manager.GetPenaltyService()
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
var banPenalty =
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
{
ServerLogger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
E.Origin.ToString(), guid);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
Utilities.IW4MAdminClient(this), true);
}
}
}
}
lock (ChatHistory) lock (ChatHistory)
{ {
while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0)) while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0))
@ -743,7 +816,7 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin) private async Task OnClientUpdate(EFClient origin)
{ {
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId); var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
if (client == null) if (client == null)
{ {
@ -788,10 +861,16 @@ namespace IW4MAdmin
/// array index 2 = updated clients /// array index 2 = updated clients
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync() async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{ {
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(Manager.CancellationToken); var statusResponse = await this.GetStatusAsync(token);
if (statusResponse is null)
{
return null;
}
var polledClients = statusResponse.Clients.AsEnumerable(); var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots) if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -903,22 +982,16 @@ namespace IW4MAdmin
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token); await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
} }
foreach (var plugin in Manager.Plugins)
{
await plugin.OnUnloadAsync();
}
} }
DateTime start = DateTime.Now; private DateTime _lastMessageSent = DateTime.Now;
DateTime playerCountStart = DateTime.Now; private DateTime _lastPlayerCount = DateTime.Now;
DateTime lastCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts) public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{ {
try try
{ {
if (cts.IsCancellationRequested) if (token.IsCancellationRequested)
{ {
await ShutdownInternal(); await ShutdownInternal();
return true; return true;
@ -926,17 +999,24 @@ namespace IW4MAdmin
try try
{ {
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue && Utilities.IsDevelopment) if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue &&
Utilities.IsDevelopment)
{ {
return true; return true;
} }
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync(token);
foreach (var disconnectingClient in polledClients[1].Where(_client => !_client.IsZombieClient /* ignores "fake" zombie clients */)) if (polledClients is null)
{
return true;
}
foreach (var disconnectingClient in polledClients[1]
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{ {
disconnectingClient.CurrentServer = this; disconnectingClient.CurrentServer = this;
var e = new GameEvent() var e = new GameEvent
{ {
Type = GameEvent.EventType.PreDisconnect, Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient, Origin = disconnectingClient,
@ -949,23 +1029,20 @@ namespace IW4MAdmin
} }
// this are our new connecting clients // this are our new connecting clients
foreach (var client in polledClients[0]) foreach (var client in polledClients[0].Where(client =>
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
{ {
// note: this prevents players in ZMBI state from being registered with no name
if (string.IsNullOrEmpty(client.Name) || (client.Ping == 999 && !client.IsBot))
{
continue;
}
client.CurrentServer = this; client.CurrentServer = this;
var e = new GameEvent() client.GameName = (Reference.Game)GameName;
var e = new GameEvent
{ {
Type = GameEvent.EventType.PreConnect, Type = GameEvent.EventType.PreConnect,
Origin = client, Origin = client,
Owner = this, Owner = this,
IsBlocking = true, IsBlocking = true,
Extra = client.GetAdditionalProperty<string>("BotGuid"), Extra = client.GetAdditionalProperty<string>("BotGuid"),
Source = GameEvent.EventSource.Status Source = GameEvent.EventSource.Status,
}; };
Manager.AddEvent(e); Manager.AddEvent(e);
@ -976,19 +1053,19 @@ namespace IW4MAdmin
foreach (var client in polledClients[2]) foreach (var client in polledClients[2])
{ {
client.CurrentServer = this; client.CurrentServer = this;
var e = new GameEvent() var gameEvent = new GameEvent
{ {
Type = GameEvent.EventType.Update, Type = GameEvent.EventType.Update,
Origin = client, Origin = client,
Owner = this Owner = this
}; };
Manager.AddEvent(e); Manager.AddEvent(gameEvent);
} }
if (Throttled) if (Throttled)
{ {
var _event = new GameEvent() var gameEvent = new GameEvent
{ {
Type = GameEvent.EventType.ConnectionRestored, Type = GameEvent.EventType.ConnectionRestored,
Owner = this, Owner = this,
@ -996,65 +1073,52 @@ namespace IW4MAdmin
Target = Utilities.IW4MAdminClient(this) Target = Utilities.IW4MAdminClient(this)
}; };
Manager.AddEvent(_event); Manager.AddEvent(gameEvent);
} }
LastPoll = DateTime.Now; LastPoll = DateTime.Now;
} }
catch (NetworkException e) catch (NetworkException ex)
{ {
if (!Throttled) if (Throttled)
{ {
var _event = new GameEvent() return true;
}
var gameEvent = new GameEvent
{ {
Type = GameEvent.EventType.ConnectionLost, Type = GameEvent.EventType.ConnectionLost,
Owner = this, Owner = this,
Origin = Utilities.IW4MAdminClient(this), Origin = Utilities.IW4MAdminClient(this),
Target = Utilities.IW4MAdminClient(this), Target = Utilities.IW4MAdminClient(this),
Extra = e, Extra = ex,
Data = ConnectionErrors.ToString() Data = ConnectionErrors.ToString()
}; };
Manager.AddEvent(_event); Manager.AddEvent(gameEvent);
return true;
}
finally
{
RunServerCollection();
} }
if (DateTime.Now - _lastMessageSent <=
TimeSpan.FromSeconds(Manager.GetApplicationSettings().Configuration().AutoMessagePeriod) ||
BroadcastMessages.Count <= 0 || ClientNum <= 0)
{
return true; return true;
} }
LastMessage = DateTime.Now - start;
lastCount = DateTime.Now;
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
// update the player history
if (lastCount - playerCountStart >= appConfig.ServerDataCollectionInterval)
{
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
appConfig.ServerDataCollectionInterval.TotalMinutes);
while ( ClientHistory.Count > maxItems)
{
ClientHistory.Dequeue();
}
ClientHistory.Enqueue(new PlayerHistory(ClientNum));
playerCountStart = DateTime.Now;
}
// send out broadcast messages // send out broadcast messages
if (LastMessage.TotalSeconds > Manager.GetApplicationSettings().Configuration().AutoMessagePeriod var messages =
&& BroadcastMessages.Count > 0 (await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(
&& ClientNum > 0) Environment.NewLine);
{ await BroadcastAsync(messages, token: Manager.CancellationToken);
string[] messages = (await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(Environment.NewLine);
foreach (string message in messages) NextMessage = NextMessage == BroadcastMessages.Count - 1 ? 0 : NextMessage + 1;
{ _lastMessageSent = DateTime.Now;
Broadcast(message);
}
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
start = DateTime.Now;
}
return true; return true;
} }
@ -1072,6 +1136,7 @@ namespace IW4MAdmin
{ {
ServerLogger.LogWarning(e, "Undesirable exception occured during processing updates"); ServerLogger.LogWarning(e, "Undesirable exception occured during processing updates");
} }
return false; return false;
} }
@ -1081,11 +1146,40 @@ namespace IW4MAdmin
{ {
ServerLogger.LogError(e, "Unexpected exception occured during processing updates"); ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
} }
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]")); Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
return false; return false;
} }
} }
private void RunServerCollection()
{
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
if (DateTime.Now - _lastPlayerCount < appConfig?.ServerDataCollectionInterval)
{
return;
}
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
appConfig.ServerDataCollectionInterval.TotalMinutes);
while (ClientHistory.ClientCounts.Count > maxItems)
{
ClientHistory.ClientCounts.RemoveAt(0);
}
ClientHistory.ClientCounts.Add(new ClientCountSnapshot
{
ClientCount = ClientNum,
ConnectionInterrupted = Throttled,
Time = DateTime.UtcNow,
Map = CurrentMap.Name
});
_lastPlayerCount = DateTime.Now;
}
public async Task Initialize() public async Task Initialize()
{ {
try try
@ -1204,28 +1298,17 @@ namespace IW4MAdmin
this.GamePassword = gamePassword.Value; this.GamePassword = gamePassword.Value;
UpdateMap(mapname); UpdateMap(mapname);
if (RconParser.CanGenerateLogPath) if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
{ {
bool needsRestart = false;
if (logsync.Value == 0) if (logsync.Value == 0)
{ {
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4 await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true;
} }
if (string.IsNullOrWhiteSpace(logfile.Value)) if (string.IsNullOrWhiteSpace(logfile.Value))
{ {
logfile.Value = "games_mp.log"; logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken); await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
needsRestart = true;
}
if (needsRestart)
{
// disabling this for the time being
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");*/
} }
// this DVAR isn't set until the a map is loaded // this DVAR isn't set until the a map is loaded
@ -1432,6 +1515,11 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString()); ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame) if (activeClient.IsIngame)
{ {
var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
@ -1461,6 +1549,11 @@ namespace IW4MAdmin
activeClient.SetLevel(Permission.Banned, originClient); activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame) if (activeClient.IsIngame)
{ {
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString()); ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());
@ -1489,7 +1582,7 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString()); ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient); targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId, await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress); targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty); await Manager.GetPenaltyService().Create(unbanPenalty);
} }

View File

@ -27,6 +27,7 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Helpers; using Data.Helpers;
using Integrations.Source.Extensions; using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization; using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -77,8 +78,12 @@ namespace IW4MAdmin.Application
/// <param name="e"></param> /// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e) private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{ {
_serverManager?.Stop(); if (_serverManager is not null)
if (_applicationTask != null) {
await _serverManager.Stop();
}
if (_applicationTask is not null)
{ {
await _applicationTask; await _applicationTask;
} }
@ -154,7 +159,10 @@ namespace IW4MAdmin.Application
Console.WriteLine(e.Message); Console.WriteLine(e.Message);
} }
_serverManager?.Stop(); if (_serverManager is not null)
{
await _serverManager?.Stop();
}
Console.WriteLine(exitMessage); Console.WriteLine(exitMessage);
await Console.In.ReadAsync(new char[1], 0, 1); await Console.In.ReadAsync(new char[1], 0, 1);
@ -405,7 +413,9 @@ namespace IW4MAdmin.Application
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>() .AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>() .AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>() .AddSingleton<IEntityService<EFClient>, ClientService>()
#pragma warning disable CS0618
.AddSingleton<IMetaService, MetaService>() .AddSingleton<IMetaService, MetaService>()
#pragma warning restore CS0618
.AddSingleton<IMetaServiceV2, MetaServiceV2>() .AddSingleton<IMetaServiceV2, MetaServiceV2>()
.AddSingleton<ClientService>() .AddSingleton<ClientService>()
.AddSingleton<PenaltyService>() .AddSingleton<PenaltyService>()
@ -438,7 +448,10 @@ namespace IW4MAdmin.Application
.AddSingleton<IServerDataViewer, ServerDataViewer>() .AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>() .AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>() .AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>() .AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);

View File

@ -83,7 +83,6 @@ namespace IW4MAdmin.Application.Meta
Value = lastMapMeta.Value, Value = lastMapMeta.Value,
ShouldDisplay = true, ShouldDisplay = true,
Type = MetaType.Information, Type = MetaType.Information,
Column = 1,
Order = 6 Order = 6
}); });
} }
@ -101,8 +100,7 @@ namespace IW4MAdmin.Application.Meta
Value = lastServerMeta.Value, Value = lastServerMeta.Value,
ShouldDisplay = true, ShouldDisplay = true,
Type = MetaType.Information, Type = MetaType.Information,
Column = 0, Order = 7
Order = 6
}); });
} }
@ -120,8 +118,7 @@ namespace IW4MAdmin.Application.Meta
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"], Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(), Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
ShouldDisplay = true, ShouldDisplay = true,
Column = 1, Order = 8,
Order = 0,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -131,8 +128,7 @@ namespace IW4MAdmin.Application.Meta
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"], Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(), Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true, ShouldDisplay = true,
Column = 1, Order = 9,
Order = 1,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -142,8 +138,7 @@ namespace IW4MAdmin.Application.Meta
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"], Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(), Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true, ShouldDisplay = true,
Column = 1, Order = 10,
Order = 2,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -154,8 +149,7 @@ namespace IW4MAdmin.Application.Meta
Value = client.Connections.ToString("#,##0", Value = client.Connections.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
ShouldDisplay = true, ShouldDisplay = true,
Column = 1, Order = 11,
Order = 3,
Type = MetaType.Information Type = MetaType.Information
}); });
@ -167,8 +161,7 @@ namespace IW4MAdmin.Application.Meta
? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"]
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"], : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
IsSensitive = true, IsSensitive = true,
Column = 1, Order = 12,
Order = 4,
Type = MetaType.Information Type = MetaType.Information
}); });

View File

@ -11,6 +11,7 @@ using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
@ -19,7 +20,8 @@ namespace IW4MAdmin.Application.Meta
/// implementation of IResourceQueryHelper /// implementation of IResourceQueryHelper
/// used to pull in penalties applied to a given client id /// used to pull in penalties applied to a given client id
/// </summary> /// </summary>
public class ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> public class
ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
@ -33,7 +35,8 @@ namespace IW4MAdmin.Application.Meta
_appConfig = appConfig; _appConfig = appConfig;
} }
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(
ClientPaginationRequest query)
{ {
var linkedPenaltyType = Utilities.LinkedPenaltyTypes(); var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
await using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
@ -48,13 +51,20 @@ namespace IW4MAdmin.Application.Meta
linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId); linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId);
IQueryable<EFPenalty> iqIpLinkedPenalties = null; IQueryable<EFPenalty> iqIpLinkedPenalties = null;
IQueryable<EFPenalty> identifierPenalties = null;
if (!_appConfig.EnableImplicitAccountLinking) if (!_appConfig.EnableImplicitAccountLinking)
{ {
var usedIps = await ctx.Aliases.AsNoTracking() var usedIps = await ctx.Aliases.AsNoTracking()
.Where(alias => (alias.LinkId == linkId.AliasLinkId || alias.AliasId == linkId.CurrentAliasId) && alias.IPAddress != null) .Where(alias =>
(alias.LinkId == linkId.AliasLinkId || alias.AliasId == linkId.CurrentAliasId) &&
alias.IPAddress != null)
.Select(alias => alias.IPAddress).ToListAsync(); .Select(alias => alias.IPAddress).ToListAsync();
identifierPenalties = ctx.PenaltyIdentifiers.AsNoTracking().Where(identifier =>
identifier.IPv4Address != null && usedIps.Contains(identifier.IPv4Address))
.Select(id => id.Penalty);
var aliasedIds = await ctx.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress)) var aliasedIds = await ctx.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress))
.Select(alias => alias.LinkId) .Select(alias => alias.LinkId)
.ToListAsync(); .ToListAsync();
@ -71,6 +81,11 @@ namespace IW4MAdmin.Application.Meta
iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties); iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties);
} }
if (identifierPenalties != null)
{
iqAllPenalties = iqPenalties.Union(identifierPenalties);
}
var penalties = await iqAllPenalties var penalties = await iqAllPenalties
.Where(_penalty => _penalty.When < query.Before) .Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When) .OrderByDescending(_penalty => _penalty.When)
@ -97,7 +112,7 @@ namespace IW4MAdmin.Application.Meta
{ {
// todo: maybe actually count // todo: maybe actually count
RetrievedResultCount = penalties.Count, RetrievedResultCount = penalties.Count,
Results = penalties Results = penalties.Distinct()
}; };
} }
} }

View File

@ -0,0 +1,12 @@
using System;
using System.Threading;
namespace IW4MAdmin.Application.Misc;
public class AsyncResult : IAsyncResult
{
public object AsyncState { get; set; }
public WaitHandle AsyncWaitHandle { get; set; }
public bool CompletedSynchronously { get; set; }
public bool IsCompleted { get; set; }
}

View File

@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
builder.Append(header); builder.Append(header);
builder.Append(config.NoticeLineSeparator); builder.Append(config.NoticeLineSeparator);
// build the reason // build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense); var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
if (isNewLineSeparator) if (isNewLineSeparator)
{ {

View File

@ -10,6 +10,7 @@ namespace IW4MAdmin.Application.Misc
{ {
public event EventHandler<GameEvent> OnClientDisconnect; public event EventHandler<GameEvent> OnClientDisconnect;
public event EventHandler<GameEvent> OnClientConnect; public event EventHandler<GameEvent> OnClientConnect;
public event EventHandler<GameEvent> OnClientMetaUpdated;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -29,10 +30,15 @@ namespace IW4MAdmin.Application.Misc
OnClientConnect?.Invoke(this, gameEvent); OnClientConnect?.Invoke(this, gameEvent);
} }
if (gameEvent.Type == GameEvent.EventType.Disconnect) if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
{ {
OnClientDisconnect?.Invoke(this, gameEvent); OnClientDisconnect?.Invoke(this, gameEvent);
} }
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
{
OnClientMetaUpdated?.Invoke(this, gameEvent);
}
} }
catch (Exception ex) catch (Exception ex)

View File

@ -0,0 +1,13 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc;
public class GeoLocationResult : IGeoLocationResult
{
public string Country { get; set; }
public string CountryCode { get; set; }
public string Region { get; set; }
public string ASN { get; set; }
public string Timezone { get; set; }
public string Organization { get; set; }
}

View File

@ -0,0 +1,40 @@
using System;
using System.Threading.Tasks;
using MaxMind.GeoIP2;
using MaxMind.GeoIP2.Responses;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc;
public class GeoLocationService : IGeoLocationService
{
private readonly string _sourceAddress;
public GeoLocationService(string sourceAddress)
{
_sourceAddress = sourceAddress;
}
public Task<IGeoLocationResult> Locate(string address)
{
CountryResponse country = null;
try
{
using var reader = new DatabaseReader(_sourceAddress);
reader.TryCountry(address, out country);
}
catch
{
// ignored
}
var response = new GeoLocationResult
{
Country = country?.Country.Name ?? "Unknown",
CountryCode = country?.Country.IsoCode ?? ""
};
return Task.FromResult((IGeoLocationResult)response);
}
}

View File

@ -0,0 +1,132 @@
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)
{
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
.FirstOrDefault(plugin => plugin.Name == source);
if (plugin is not ScriptPlugin scriptPlugin)
{
return;
}
var wrappedDelegate = (int? clientId, Reference.Game? game, CancellationToken token) =>
Task.FromResult(
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, 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))
{
_interactions.TryAdd(interactionName, interactionRegistration);
}
else
{
_interactions[interactionName] = interactionRegistration;
}
}
public void UnregisterInteraction(string interactionName)
{
if (_interactions.ContainsKey(interactionName))
{
_interactions.TryRemove(interactionName, out _);
}
}
public async Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return (await Task.WhenAll(_interactions.Select(async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for interaction {InteractionName} and ClientId {ClientId}", kvp.Key,
clientId);
return null;
}
}))).Where(interaction => interaction is not null);
}
public async Task<string> ProcessInteraction(string interactionId, int? clientId = null,
Reference.Game? game = 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](clientId, game, token);
if (interaction.Action is not null)
{
return await interaction.Action(clientId, game, 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, clientId, game, token);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not process interaction for interaction {InteractionName} and ClientId {ClientId}",
interactionId,
clientId);
}
return null;
}
}

View File

@ -7,7 +7,10 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models; using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
@ -19,13 +22,15 @@ public class MetaServiceV2 : IMetaServiceV2
{ {
private readonly IDictionary<MetaType, List<dynamic>> _metaActions; private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory) public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
{ {
_logger = logger; _logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>(); _metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory; _contextFactory = contextFactory;
_serviceProvider = serviceProvider;
} }
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId, public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
} }
await context.SaveChangesAsync(token); await context.SaveChangesAsync(token);
var manager = _serviceProvider.GetRequiredService<IManager>();
var matchingClient = manager.GetActiveClients().FirstOrDefault(client => client.ClientId == clientId);
var server = matchingClient?.CurrentServer ?? manager.GetServers().FirstOrDefault();
if (server is not null)
{
manager.AddEvent(new GameEvent
{
Type = GameEvent.EventType.MetaUpdated,
Origin = matchingClient ?? new EFClient
{
ClientId = clientId
},
Data = metaValue,
Extra = metaKey,
Owner = server
});
}
} }
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId, public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,
@ -207,7 +232,7 @@ public class MetaServiceV2 : IMetaServiceV2
if (metaValue is null) if (metaValue is null)
{ {
_logger.LogWarning("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId); _logger.LogDebug("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId);
return default; return default;
} }
@ -439,61 +464,14 @@ public class MetaServiceV2 : IMetaServiceV2
return ProcessInformationMeta(allMeta); return ProcessInformationMeta(allMeta);
} }
var meta = await _metaActions[metaType][0](request) as IEnumerable<T>; var meta = await _metaActions[metaType][0](request, token) as IEnumerable<T>;
return meta; return meta;
} }
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
{ {
var metaList = meta.ToList(); return meta;
var metaWithColumn = metaList
.Where(m => m.Column != null)
.ToList();
var columnGrouping = metaWithColumn
.GroupBy(m => m.Column)
.ToList();
var metaToSort = metaList.Except(metaWithColumn).ToList();
var table = columnGrouping.Select(metaItem => new List<T>(metaItem)).ToList();
while (metaToSort.Count > 0)
{
var sortingMeta = metaToSort.First();
int IndexOfSmallestColumn()
{
var index = 0;
var smallestColumnSize = int.MaxValue;
for (var i = 0; i < table.Count; i++)
{
if (table[i].Count >= smallestColumnSize)
{
continue;
}
smallestColumnSize = table[i].Count;
index = i;
}
return index;
}
var columnIndex = IndexOfSmallestColumn();
sortingMeta.Column = columnIndex;
sortingMeta.Order = columnGrouping
.First(group => group.Key == columnIndex)
.Count();
table[columnIndex].Add(sortingMeta);
metaToSort.Remove(sortingMeta);
}
return metaList;
} }
private static bool ValidArgs(string key, int clientId) => !string.IsNullOrWhiteSpace(key) && clientId > 0; private static bool ValidArgs(string key, int clientId) => !string.IsNullOrWhiteSpace(key) && clientId > 0;

View File

@ -42,6 +42,7 @@ namespace IW4MAdmin.Application.Misc
private Engine _scriptEngine; private Engine _scriptEngine;
private readonly string _fileName; private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing = new(1, 1); private readonly SemaphoreSlim _onProcessing = new(1, 1);
private readonly SemaphoreSlim _onDvarActionComplete = new(1, 1);
private bool _successfullyLoaded; private bool _successfullyLoaded;
private readonly List<string> _registeredCommandNames; private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -116,7 +117,8 @@ namespace IW4MAdmin.Application.Misc
typeof(System.Net.Http.HttpClient).Assembly, typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly, typeof(EFClient).Assembly,
typeof(Utilities).Assembly, typeof(Utilities).Assembly,
typeof(Encoding).Assembly typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly
}) })
.CatchClrExceptions() .CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter())); .AddObjectConverter(new PermissionLevelToStringConverter()));
@ -276,8 +278,8 @@ namespace IW4MAdmin.Application.Misc
{ {
_logger.LogDebug("OnLoad executing for {Name}", Name); _logger.LogDebug("OnLoad executing for {Name}", Name);
_scriptEngine.SetValue("_manager", manager); _scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", GetDvarAsync); _scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", SetDvarAsync); _scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)"); _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask; return Task.CompletedTask;
@ -337,13 +339,49 @@ namespace IW4MAdmin.Application.Misc
return Task.CompletedTask; return Task.CompletedTask;
} }
public T ExecuteAction<T>(Delegate action, params object[] param)
{
try
{
_onProcessing.Wait();
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
public T WrapDelegate<T>(Delegate act, params object[] args)
{
try
{
_onProcessing.Wait();
return (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
/// <summary> /// <summary>
/// finds declared script commands in the script plugin /// finds declared script commands in the script plugin
/// </summary> /// </summary>
/// <param name="commands">commands value from jint parser</param> /// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param> /// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns> /// <returns></returns>
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory) private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
IScriptCommandFactory scriptCommandFactory)
{ {
var commandList = new List<IManagerCommand>(); var commandList = new List<IManagerCommand>();
@ -454,65 +492,86 @@ namespace IW4MAdmin.Application.Misc
return commandList; return commandList;
} }
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted) private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{ {
Task.Run<Task>(async () => void OnComplete(IAsyncResult result)
{ {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
string result = null;
var success = true;
try try
{ {
result = (await server.GetDvarAsync<string>(dvarName, token: tokenSource.Token)).Value; var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
}
catch _logger.LogDebug("Waiting for onDvarActionComplete -> get");
{ _onDvarActionComplete.Wait();
success = false; _logger.LogDebug("Completed wait for onDvarActionComplete -> get");
}
await _onProcessing.WaitAsync();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined, onCompleted.DynamicInvoke(JsValue.Undefined,
new[] new[]
{ {
JsValue.FromObject(_scriptEngine, server), JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName), JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, result), JsValue.FromObject(_scriptEngine, value),
JsValue.FromObject(_scriptEngine, success), JsValue.FromObject(_scriptEngine, success)
}); });
} }
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex, "Could complete BeginGetDvar for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
}
finally
{
if (_onDvarActionComplete.CurrentCount == 0)
{
_onDvarActionComplete.Release();
}
}
}
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginGetDvar(dvarName, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
}
finally finally
{ {
if (_onProcessing.CurrentCount == 0) OnComplete(result);
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{ {
_onProcessing.Release(); _onProcessing.Release();
} }
} }
}); }, tokenSource.Token);
} }
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{ private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
Task.Run<Task>(async () =>
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5)); tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
var success = true;
void OnComplete(IAsyncResult result)
{
try try
{ {
await server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token); var success = (bool)result.AsyncState;
}
catch
{
success = false;
}
await _onProcessing.WaitAsync(); _logger.LogDebug("Waiting for onDvarActionComplete -> set");
try _onDvarActionComplete.Wait();
{ _logger.LogDebug("Completed wait for onDvarActionComplete -> set");
onCompleted.DynamicInvoke(JsValue.Undefined, onCompleted.DynamicInvoke(JsValue.Undefined,
new[] new[]
{ {
@ -522,15 +581,44 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success) JsValue.FromObject(_scriptEngine, success)
}); });
} }
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex, "Could complete BeginSetDvar for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name);
}
finally finally
{ {
if (_onProcessing.CurrentCount == 0) if (_onDvarActionComplete.CurrentCount == 0)
{
_onDvarActionComplete.Release();
}
}
}
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
}
finally
{
OnComplete(result);
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{ {
_onProcessing.Release(); _onProcessing.Release();
} }
} }
}); }, tokenSource.Token);
} }
} }

View File

@ -111,7 +111,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{ {
return; return;
} }
_logger.LogDebug("-Releasing OnTick for timer");
_onDependentAction?.Release(1); _onDependentAction?.Release(1);
} }
private void OnTick() private void OnTick()
@ -128,7 +128,8 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_onRunningTick.Reset(); _onRunningTick.Reset();
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously // the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
_onDependentAction?.WaitAsync().Wait(); _onDependentAction?.Wait();
_logger.LogDebug("+Running OnTick for timer");
var start = DateTime.Now; var start = DateTime.Now;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }); _jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds); _logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);

View File

@ -94,7 +94,8 @@ namespace IW4MAdmin.Application.Misc
PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes, PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
ServerId = await server.GetIdForServer(), ServerId = await server.GetIdForServer(),
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token), MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
ClientCount = server.ClientNum ClientCount = server.ClientNum,
ConnectionInterrupted = server.Throttled,
})); }));
return data; return data;

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server; using Data.Models.Server;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache; private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache; private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache; private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan = private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10); Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache, public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache, IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache) IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{ {
_logger = logger; _logger = logger;
_snapshotCache = snapshotCache; _snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache; _serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache; _clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
} }
public async Task<(int?, DateTime?)> public async Task<(int?, DateTime?)>
@ -135,7 +138,9 @@ namespace IW4MAdmin.Application.Misc
{ {
snapshot.ServerId, snapshot.ServerId,
snapshot.CapturedAt, snapshot.CapturedAt,
snapshot.ClientCount snapshot.ClientCount,
snapshot.ConnectionInterrupted,
MapName = snapshot.Map.Name,
}) })
.OrderBy(snapshot => snapshot.CapturedAt) .OrderBy(snapshot => snapshot.CapturedAt)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -143,8 +148,8 @@ namespace IW4MAdmin.Application.Misc
return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
{ {
ServerId = byServer.Key, ServerId = byServer.Key,
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot() ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot
{Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList() { Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount, ConnectionInterrupted = snapshot.ConnectionInterrupted ?? false, Map = snapshot.MapName}).ToList()
}).ToList(); }).ToList();
}, nameof(_clientHistoryCache), TimeSpan.MaxValue); }, nameof(_clientHistoryCache), TimeSpan.MaxValue);
@ -158,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
return Enumerable.Empty<ClientHistoryInfo>(); return Enumerable.Empty<ClientHistoryInfo>();
} }
} }
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
return 0;
}
}
} }
} }

View File

@ -9,40 +9,41 @@ namespace IW4MAdmin.Application.Misc
{ {
internal class TokenAuthentication : ITokenAuthentication internal class TokenAuthentication : ITokenAuthentication
{ {
private readonly ConcurrentDictionary<long, TokenState> _tokens; private readonly ConcurrentDictionary<int, TokenState> _tokens;
private readonly RandomNumberGenerator _random; private readonly RandomNumberGenerator _random;
private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120); private static readonly TimeSpan TimeoutPeriod = new(0, 0, 120);
private const short TokenLength = 4; private const short TokenLength = 4;
public TokenAuthentication() public TokenAuthentication()
{ {
_tokens = new ConcurrentDictionary<long, TokenState>(); _tokens = new ConcurrentDictionary<int, TokenState>();
_random = RandomNumberGenerator.Create(); _random = RandomNumberGenerator.Create();
} }
public bool AuthorizeToken(long networkId, string token) public bool AuthorizeToken(ITokenIdentifier authInfo)
{ {
var authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token; var authorizeSuccessful = _tokens.ContainsKey(authInfo.ClientId) &&
_tokens[authInfo.ClientId].Token == authInfo.Token;
if (authorizeSuccessful) if (authorizeSuccessful)
{ {
_tokens.TryRemove(networkId, out _); _tokens.TryRemove(authInfo.ClientId, out _);
} }
return authorizeSuccessful; return authorizeSuccessful;
} }
public TokenState GenerateNextToken(long networkId) public TokenState GenerateNextToken(ITokenIdentifier authInfo)
{ {
TokenState state; TokenState state;
if (_tokens.ContainsKey(networkId)) if (_tokens.ContainsKey(authInfo.ClientId))
{ {
state = _tokens[networkId]; state = _tokens[authInfo.ClientId];
if ((DateTime.Now - state.RequestTime) > TimeoutPeriod) if (DateTime.Now - state.RequestTime > TimeoutPeriod)
{ {
_tokens.TryRemove(networkId, out _); _tokens.TryRemove(authInfo.ClientId, out _);
} }
else else
@ -53,17 +54,16 @@ namespace IW4MAdmin.Application.Misc
state = new TokenState state = new TokenState
{ {
NetworkId = networkId,
Token = _generateToken(), Token = _generateToken(),
TokenDuration = TimeoutPeriod TokenDuration = TimeoutPeriod
}; };
_tokens.TryAdd(networkId, state); _tokens.TryAdd(authInfo.ClientId, state);
// perform some housekeeping so we don't have built up tokens if they're not ever used // perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens) foreach (var (key, value) in _tokens)
{ {
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod) if (DateTime.Now - value.RequestTime > TimeoutPeriod)
{ {
_tokens.TryRemove(key, out _); _tokens.TryRemove(key, out _);
} }

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -19,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private static string _botIpIndicator = "00000000.";
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory) public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
{ {
@ -51,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5); Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7); Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$"; Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)?$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -80,6 +82,7 @@ namespace IW4MAdmin.Application.RConParsers
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default) public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
{ {
command = command.FormatMessageForEngine(Configuration);
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray(); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
@ -102,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
lineSplit = Array.Empty<string>(); lineSplit = Array.Empty<string>();
} }
var response = string.Join('\n', lineSplit).TrimEnd('\0'); var response = string.Join('\n', lineSplit).Replace("\n", "").TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern); var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") || if (response.Contains("Unknown command") ||
@ -140,6 +143,30 @@ namespace IW4MAdmin.Application.RConParsers
}; };
} }
public void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default)
{
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
{
if (action.Exception is null && !action.IsCanceled)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (false, (string)null)
});
}
}, CancellationToken.None);
}
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default) public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
@ -149,16 +176,16 @@ namespace IW4MAdmin.Application.RConParsers
return new StatusResponse return new StatusResponse
{ {
Clients = ClientsFromStatus(response).ToArray(), Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern), Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern), GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern), Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern) MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus)
}; };
} }
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, string groupPattern) private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, ParserRegex parserRegex)
{ {
if (string.IsNullOrEmpty(groupPattern)) if (string.IsNullOrEmpty(parserRegex.Pattern))
{ {
return default; return default;
} }
@ -166,10 +193,10 @@ namespace IW4MAdmin.Application.RConParsers
string value = null; string value = null;
foreach (var line in response) foreach (var line in response)
{ {
var regex = Regex.Match(line, groupPattern); var regex = Regex.Match(line, parserRegex.Pattern);
if (regex.Success) if (regex.Success && parserRegex.GroupMapping.ContainsKey(groupType))
{ {
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString(); value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
} }
} }
@ -195,6 +222,31 @@ namespace IW4MAdmin.Application.RConParsers
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
} }
public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback,
CancellationToken token = default)
{
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
{
if (action.Exception is null && !action.IsCanceled)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = false
});
}
}, CancellationToken.None);
}
private List<EFClient> ClientsFromStatus(string[] Status) private List<EFClient> ClientsFromStatus(string[] Status)
{ {
List<EFClient> StatusPlayers = new List<EFClient>(); List<EFClient> StatusPlayers = new List<EFClient>();
@ -239,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
long networkId; long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString; string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP(); var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
.Contains(_botIpIndicator))
{
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
}
try try
{ {
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
@ -255,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
continue; continue;
} }
var client = new EFClient() var client = new EFClient
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias
{ {
Name = name, Name = name,
IPAddress = ip IPAddress = ip
@ -309,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) : (T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default; default;
public TimeSpan OverrideTimeoutForCommand(string command) public TimeSpan? OverrideTimeoutForCommand(string command)
{ {
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) || if (string.IsNullOrEmpty(command))
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
{ {
return TimeSpan.FromSeconds(30);
}
return TimeSpan.Zero; return TimeSpan.Zero;
} }
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
}
var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
{
return TimeSpan.FromSeconds(timeoutValue.Value);
}
return null;
}
} }
} }

View File

@ -26,12 +26,14 @@ namespace IW4MAdmin.Application.RConParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public IDictionary<string, int?> OverrideCommandTimeouts { get; set; } = new Dictionary<string, int?>();
public int NoticeMaximumLines { get; set; } = 8; public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50; public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine; public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public int? DefaultRConPort { get; set; } public int? DefaultRConPort { get; set; }
public string DefaultInstallationDirectoryHint { get; set; } public string DefaultInstallationDirectoryHint { get; set; }
public short FloodProtectInterval { get; set; } = 750; public short FloodProtectInterval { get; set; } = 750;
public bool ShouldRemoveDiacritics { get; set; }
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
{ {
@ -46,7 +48,7 @@ namespace IW4MAdmin.Application.RConParsers
{ColorCodes.White.ToString(), "^7"}, {ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"}, {ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"}, {ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), ":^"}, {ColorCodes.Wildcard.ToString(), "^:"}
}; };
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
@ -58,6 +60,25 @@ namespace IW4MAdmin.Application.RConParsers
StatusHeader = parserRegexFactory.CreateParserRegex(); StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = parserRegexFactory.CreateParserRegex(); HostnameStatus = parserRegexFactory.CreateParserRegex();
MaxPlayersStatus = parserRegexFactory.CreateParserRegex(); MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
const string mapRotateCommand = "map_rotate";
const string mapCommand = "map";
const string fastRestartCommand = "fast_restart";
const string quitCommand = "quit";
foreach (var command in new[] { mapRotateCommand, mapCommand, fastRestartCommand})
{
if (!OverrideCommandTimeouts.ContainsKey(command))
{
OverrideCommandTimeouts.Add(command, 45);
}
}
if (!OverrideCommandTimeouts.ContainsKey(quitCommand))
{
OverrideCommandTimeouts.Add(quitCommand, 0); // we don't want to wait for a response when we quit the server
}
} }
} }
} }

Binary file not shown.

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -9,6 +10,11 @@ namespace Data.Abstractions
{ {
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName, void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false); TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default); Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
} }
} }

View File

@ -85,7 +85,15 @@ namespace Data.Context
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
// make network id unique // make network id unique
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); }); modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(e => e.NetworkId);
entity.HasAlternateKey(client => new
{
client.NetworkId,
client.GameName
});
});
modelBuilder.Entity<EFPenalty>(entity => modelBuilder.Entity<EFPenalty>(entity =>
{ {
@ -120,6 +128,9 @@ namespace Data.Context
ent.Property(_alias => _alias.SearchableName).HasMaxLength(24); ent.Property(_alias => _alias.SearchableName).HasMaxLength(24);
ent.HasIndex(_alias => _alias.SearchableName); ent.HasIndex(_alias => _alias.SearchableName);
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress}); ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress});
ent.Property(alias => alias.SearchableIPAddress)
.HasComputedColumnSql(@"((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", stored: true);
ent.HasIndex(alias => alias.SearchableIPAddress);
}); });
modelBuilder.Entity<EFMeta>(ent => modelBuilder.Entity<EFMeta>(ent =>

View File

@ -8,7 +8,7 @@
<PackageId>RaidMax.IW4MAdmin.Data</PackageId> <PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title> <Title>RaidMax.IW4MAdmin.Data</Title>
<Authors /> <Authors />
<PackageVersion>1.2.0</PackageVersion> <PackageVersion>2022.10.11.1</PackageVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
@ -15,8 +17,8 @@ namespace Data.Helpers
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates = private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
new ConcurrentDictionary<string, CacheState<TReturnType>>(); private readonly object _defaultKey = new();
private bool _autoRefresh; private bool _autoRefresh;
private const int DefaultExpireMinutes = 15; private const int DefaultExpireMinutes = 15;
@ -51,10 +53,24 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key, public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false) TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
if (_cacheStates.ContainsKey(key)) SetCacheItem(getter, key, null, expirationTime, autoRefresh);
}
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
_logger.LogDebug("Cache key {Key} is already added", key); ids ??= new[] { _defaultKey };
return;
if (!_cacheStates.ContainsKey(key))
{
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
foreach (var id in ids)
{
if (_cacheStates[key].ContainsKey(id))
{
continue;
} }
var state = new CacheState<TReturnType> var state = new CacheState<TReturnType>
@ -64,9 +80,10 @@ namespace Data.Helpers
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes) ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
}; };
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh; _autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue) if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{ {
@ -77,15 +94,20 @@ namespace Data.Helpers
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None); _timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start(); _timer.Start();
} }
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
await GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
CancellationToken cancellationToken = default)
{ {
if (!_cacheStates.ContainsKey(keyName)) if (!_cacheStates.ContainsKey(keyName))
{ {
throw new ArgumentException("No cache found for key {key}", keyName); throw new ArgumentException("No cache found for key {key}", keyName);
} }
var state = _cacheStates[keyName]; var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
// when auto refresh is off we want to check the expiration and value // when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically // when auto refresh is on, we want to only check the value, because it'll be refreshed automatically

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddConnectionInterruptedToEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "ConnectionInterrupted",
table: "EFServerSnapshot",
type: "tinyint(1)",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ConnectionInterrupted",
table: "EFServerSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddSearchableIPToEFAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "longtext",
nullable: true,
computedColumnSql: "CONCAT((IPAddress & 255), \".\", ((IPAddress >> 8) & 255), \".\", ((IPAddress >> 16) & 255), \".\", ((IPAddress >> 24) & 255))",
stored: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SearchableIPAddress",
table: "EFAlias");
}
}
}

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 AddIndexToSearchableIPToEFAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFAlias_SearchableIPAddress",
table: "EFAlias",
column: "SearchableIPAddress");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_SearchableIPAddress",
table: "EFAlias");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddGameToEFClient : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GameName",
table: "EFClients");
}
}
}

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 AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.Sql("UPDATE `EFClients` set `GameName` = 0 WHERE `GameName` IS NULL");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
try
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on EFClientMessages (TimeSent desc);");
}
catch
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on efclientmessages (TimeSent desc);");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"drop index IX_EFClientMessages_TimeSentDesc on EFClientMessages;");
}
}
}

View File

@ -64,6 +64,9 @@ namespace Data.Migrations.MySql
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<int>("GameName")
.HasColumnType("int");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
@ -87,12 +90,13 @@ namespace Data.Migrations.MySql
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -453,6 +457,8 @@ namespace Data.Migrations.MySql
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");
@ -804,6 +810,11 @@ namespace Data.Migrations.MySql
.HasMaxLength(24) .HasMaxLength(24)
.HasColumnType("varchar(24)"); .HasColumnType("varchar(24)");
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("varchar(255)")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
b.Property<string>("SearchableName") b.Property<string>("SearchableName")
.HasMaxLength(24) .HasMaxLength(24)
.HasColumnType("varchar(24)"); .HasColumnType("varchar(24)");
@ -816,6 +827,8 @@ namespace Data.Migrations.MySql
b.HasIndex("Name"); b.HasIndex("Name");
b.HasIndex("SearchableIPAddress");
b.HasIndex("SearchableName"); b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress"); b.HasIndex("Name", "IPAddress");
@ -1081,6 +1094,9 @@ namespace Data.Migrations.MySql
b.Property<int>("ClientCount") b.Property<int>("ClientCount")
.HasColumnType("int"); .HasColumnType("int");
b.Property<bool?>("ConnectionInterrupted")
.HasColumnType("tinyint(1)");
b.Property<int>("MapId") b.Property<int>("MapId")
.HasColumnType("int"); .HasColumnType("int");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddConnectionInterruptedToEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "ConnectionInterrupted",
table: "EFServerSnapshot",
type: "boolean",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ConnectionInterrupted",
table: "EFServerSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddSearchableIPToEFAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "text",
nullable: true,
computedColumnSql: "CASE WHEN \"IPAddress\" IS NULL THEN 'NULL'::text ELSE (\"IPAddress\" & 255)::text || '.'::text || ((\"IPAddress\" >> 8) & 255)::text || '.'::text || ((\"IPAddress\" >> 16) & 255)::text || '.'::text || ((\"IPAddress\" >> 24) & 255)::text END",
stored: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SearchableIPAddress",
table: "EFAlias");
}
}
}

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 AddIndexToSearchableIPToEFAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFAlias_SearchableIPAddress",
table: "EFAlias",
column: "SearchableIPAddress");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_SearchableIPAddress",
table: "EFAlias");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddGameToEFClient : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GameName",
table: "EFClients");
}
}
}

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 AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.Sql("UPDATE \"EFClients\" SET \"GameName\" = 0 WHERE \"GameName\" IS NULL");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

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 AddDescendingTimeSentIndexEFClientMessages : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"CREATE INDEX""IX_EFClientMessages_TimeSentDesc""
ON public.""EFClientMessages"" USING btree
(""TimeSent"" DESC NULLS LAST)
TABLESPACE pg_default;"
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DROP INDEX public.""IX_EFClientMessages_TimeSentDesc""");
}
}
}

View File

@ -71,6 +71,9 @@ namespace Data.Migrations.Postgresql
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<int>("GameName")
.HasColumnType("integer");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
@ -94,12 +97,13 @@ namespace Data.Migrations.Postgresql
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -472,6 +476,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");
@ -843,6 +849,11 @@ namespace Data.Migrations.Postgresql
.HasMaxLength(24) .HasMaxLength(24)
.HasColumnType("character varying(24)"); .HasColumnType("character varying(24)");
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("text")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
b.Property<string>("SearchableName") b.Property<string>("SearchableName")
.HasMaxLength(24) .HasMaxLength(24)
.HasColumnType("character varying(24)"); .HasColumnType("character varying(24)");
@ -855,6 +866,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("Name"); b.HasIndex("Name");
b.HasIndex("SearchableIPAddress");
b.HasIndex("SearchableName"); b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress"); b.HasIndex("Name", "IPAddress");
@ -1134,6 +1147,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("ClientCount") b.Property<int>("ClientCount")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<bool?>("ConnectionInterrupted")
.HasColumnType("boolean");
b.Property<int>("MapId") b.Property<int>("MapId")
.HasColumnType("integer"); .HasColumnType("integer");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddConnectionInterruptedToEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "ConnectionInterrupted",
table: "EFServerSnapshot",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ConnectionInterrupted",
table: "EFServerSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddSearchableIPToEFAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "TEXT",
nullable: true,
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SearchableIPAddress",
table: "EFAlias");
}
}
}

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 AddIndexToSearchableIPToEFAlias : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFAlias_SearchableIPAddress",
table: "EFAlias",
column: "SearchableIPAddress");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_SearchableIPAddress",
table: "EFAlias");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddGameToEFClient : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GameName",
table: "EFClients");
}
}
}

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 AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -62,6 +62,9 @@ namespace Data.Migrations.Sqlite
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("GameName")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -85,12 +88,13 @@ namespace Data.Migrations.Sqlite
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -451,6 +455,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");
@ -802,6 +808,11 @@ namespace Data.Migrations.Sqlite
.HasMaxLength(24) .HasMaxLength(24)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("TEXT")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
b.Property<string>("SearchableName") b.Property<string>("SearchableName")
.HasMaxLength(24) .HasMaxLength(24)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -814,6 +825,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("Name"); b.HasIndex("Name");
b.HasIndex("SearchableIPAddress");
b.HasIndex("SearchableName"); b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress"); b.HasIndex("Name", "IPAddress");
@ -1079,6 +1092,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("ClientCount") b.Property<int>("ClientCount")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool?>("ConnectionInterrupted")
.HasColumnType("INTEGER");
b.Property<int>("MapId") b.Property<int>("MapId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@ -63,6 +63,7 @@ namespace Data.Models.Client
public DateTime FirstConnection { get; set; } public DateTime FirstConnection { get; set; }
[Required] [Required]
public DateTime LastConnection { get; set; } public DateTime LastConnection { get; set; }
public Reference.Game GameName { get; set; } = Reference.Game.UKN;
public bool Masked { get; set; } public bool Masked { get; set; }
[Required] [Required]
public int AliasLinkId { get; set; } public int AliasLinkId { get; set; }

View File

@ -7,8 +7,6 @@ namespace Data.Models.Client.Stats
{ {
public class EFClientRankingHistory: AuditFields public class EFClientRankingHistory: AuditFields
{ {
public const int MaxRankingCount = 30;
[Key] [Key]
public long ClientRankingHistoryId { get; set; } public long ClientRankingHistoryId { get; set; }

View File

@ -86,6 +86,7 @@ namespace Data.Models.Configuration
entity.HasIndex(ranking => ranking.Ranking); entity.HasIndex(ranking => ranking.Ranking);
entity.HasIndex(ranking => ranking.ZScore); entity.HasIndex(ranking => ranking.ZScore);
entity.HasIndex(ranking => ranking.UpdatedDateTime); entity.HasIndex(ranking => ranking.UpdatedDateTime);
entity.HasIndex(ranking => ranking.CreatedDateTime);
}); });
} }
} }

View File

@ -19,6 +19,7 @@ namespace Data.Models
public string SearchableName { get; set; } public string SearchableName { get; set; }
[Required] [Required]
public int? IPAddress { get; set; } public int? IPAddress { get; set; }
public string SearchableIPAddress { get; set; }
[Required] [Required]
public DateTime DateAdded { get; set; } public DateTime DateAdded { get; set; }

View File

@ -15,7 +15,8 @@
T6 = 7, T6 = 7,
T7 = 8, T7 = 8,
SHG1 = 9, SHG1 = 9,
CSGO = 10 CSGO = 10,
H1 = 11
} }
public enum ConnectionType public enum ConnectionType

View File

@ -32,5 +32,7 @@ namespace Data.Models.Server
public EFMap Map { get; set; } public EFMap Map { get; set; }
public int ClientCount { get; set; } public int ClientCount { get; set; }
public bool? ConnectionInterrupted {get;set;}
} }
} }

View File

@ -53,7 +53,9 @@ steps:
script: | script: |
Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)' Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)'
md -Force lib\open-iconic\font\css md -Force lib\open-iconic\font\css
wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap.scss wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss
cd lib\open-iconic\font\css
(Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss
failOnStderr: true failOnStderr: true
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot' workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot'
@ -75,6 +77,7 @@ steps:
Write-Host 'Unzipping download' Write-Host 'Unzipping download'
Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath) Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath)
Write-Host 'Executing dotnet-bundle' Write-Host 'Executing dotnet-bundle'
$(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
$(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json $(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
failOnStderr: true failOnStderr: true
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore' workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore'

View File

@ -29,8 +29,9 @@ onPlayerConnect( player )
for( ;; ) for( ;; )
{ {
level waittill( "connected", player ); level waittill( "connected", player );
player setClientDvar("cl_autorecord", 1); player setClientDvars( "cl_autorecord", 1,
player setClientDvar("cl_demosKeep", 200); "cl_demosKeep", 200 );
player thread waitForFrameThread(); player thread waitForFrameThread();
player thread waitForAttack(); player thread waitForAttack();
} }
@ -60,7 +61,7 @@ getHttpString( url )
runRadarUpdates() runRadarUpdates()
{ {
interval = int(getDvar("sv_printradar_updateinterval")); interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; ) for ( ;; )
{ {
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++; i++;
} }
lastAttack = int(getTime()) - int(self.lastAttackTime); lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self); isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" ); logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

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

@ -53,7 +53,7 @@ waitForAttack()
runRadarUpdates() runRadarUpdates()
{ {
interval = int(getDvar("sv_printradar_updateinterval")); interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; ) for ( ;; )
{ {
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++; i++;
} }
lastAttack = int(getTime()) - int(self.lastAttackTime); lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self); isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" ); logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

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", "Offset",
"Strain", "Snap",
"Snap" "Strain"
] ]
``` ```

View File

@ -60,7 +60,7 @@ waitForAttack()
runRadarUpdates() runRadarUpdates()
{ {
interval = int(getDvar("sv_printradar_updateinterval")); interval = getDvarInt( "sv_printradar_updateinterval" );
for ( ;; ) for ( ;; )
{ {
@ -190,7 +190,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++; i++;
} }
lastAttack = int(getTime()) - int(self.lastAttackTime); lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self); isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" ); logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -1,10 +1,16 @@
#include common_scripts\utility; #include common_scripts\utility;
#include maps\mp\_utility; #include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util; #include maps\mp\gametypes\_hud_util;
#include maps\mp\gametypes\_playerlogic;
init() Init()
{ {
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// setup default vars // setup default vars
level.eventBus = spawnstruct(); level.eventBus = spawnstruct();
level.eventBus.inVar = "sv_iw4madmin_in"; level.eventBus.inVar = "sv_iw4madmin_in";
@ -13,6 +19,13 @@ init()
level.eventBus.timeoutKey = "timeout"; level.eventBus.timeoutKey = "timeout";
level.eventBus.timeout = 30; level.eventBus.timeout = 30;
level.commonFunctions = spawnstruct();
level.commonFunctions.SetDvar = "SetDvarIfUninitialized";
level.notifyTypes = spawnstruct();
level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized";
level.notifyTypes.integrationBootstrapInitialized = "IntegrationBootstrapInitialized";
level.clientDataKey = "clientData"; level.clientDataKey = "clientData";
level.eventTypes = spawnstruct(); level.eventTypes = spawnstruct();
@ -23,10 +36,7 @@ init()
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted"; level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested"; level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
SetDvarIfUninitialized( level.eventBus.inVar, "" ); level.iw4madminIntegrationDebug = 0;
SetDvarIfUninitialized( level.eventBus.outVar, "" );
SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
// map the event type to the handler // map the event type to the handler
level.eventCallbacks = []; level.eventCallbacks = [];
@ -34,6 +44,26 @@ init()
level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand; level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand;
level.eventCallbacks[level.eventTypes.setClientDataCompleted] = ::OnSetClientDataCompleted; 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 ) if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{ {
return; return;
@ -51,15 +81,19 @@ init()
OnPlayerConnect() OnPlayerConnect()
{ {
level endon ( "disconnect" ); level endon ( "game_ended" );
for ( ;; ) for ( ;; )
{ {
level waittill( "connected", player ); level waittill( "connected", player );
level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" ); if ( _IsBot( player ) )
{
// we don't want to track bots
continue;
}
if ( !isDefined( player.pers[level.clientDataKey] ) ) if ( !IsDefined( player.pers[level.clientDataKey] ) )
{ {
player.pers[level.clientDataKey] = spawnstruct(); player.pers[level.clientDataKey] = spawnstruct();
} }
@ -68,12 +102,6 @@ OnPlayerConnect()
player thread OnPlayerJoinedTeam(); player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators(); player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval(); player thread PlayerTrackingOnInterval();
// only toggle if it's enabled
if ( IsDefined( level.nightModeEnabled ) && level.nightModeEnabled )
{
player ToggleNightMode();
}
} }
} }
@ -84,13 +112,13 @@ OnPlayerSpawned()
for ( ;; ) for ( ;; )
{ {
self waittill( "spawned_player" ); self waittill( "spawned_player" );
self PlayerConnectEvents(); self PlayerSpawnEvents();
} }
} }
OnPlayerDisconnect() OnPlayerDisconnect()
{ {
level endon ( "disconnect" ); self endon ( "disconnect" );
for ( ;; ) for ( ;; )
{ {
@ -125,8 +153,6 @@ OnPlayerJoinedSpectators()
OnGameEnded() OnGameEnded()
{ {
level endon ( "disconnect" );
for ( ;; ) for ( ;; )
{ {
level waittill( "game_ended" ); level waittill( "game_ended" );
@ -151,29 +177,22 @@ DisplayWelcomeData()
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection ); self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
} }
PlayerConnectEvents() PlayerSpawnEvents()
{ {
self endon( "disconnect" ); self endon( "disconnect" );
if ( IsDefined( self.isHidden ) && self.isHidden )
{
self HideImpl();
}
clientData = self.pers[level.clientDataKey]; clientData = self.pers[level.clientDataKey];
// this gives IW4MAdmin some time to register the player before making the request; // 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 // although probably not necessary some users might have a slow database or poll rate
wait ( 2 ); wait ( 2 );
if ( isDefined( clientData.state ) && clientData.state == "complete" ) if ( IsDefined( clientData.state ) && clientData.state == "complete" )
{ {
return; return;
} }
self RequestClientBasicData(); self RequestClientBasicData();
// example of requesting meta from IW4MAdmin
// self RequestClientMeta( "LastServerPlayed" );
} }
PlayerTrackingOnInterval() PlayerTrackingOnInterval()
@ -192,23 +211,21 @@ PlayerTrackingOnInterval()
MonitorClientEvents() MonitorClientEvents()
{ {
level endon( "disconnect" ); level endon( "game_ended" );
self endon( "disconnect" );
for ( ;; ) for ( ;; )
{ {
level waittill( level.eventTypes.localClientEvent, client ); level waittill( level.eventTypes.localClientEvent, client );
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
{
self IPrintLn( "Processing Event " + client.event.type + "-" + client.event.subtype );
}
eventHandler = level.eventCallbacks[client.event.type]; eventHandler = level.eventCallbacks[client.event.type];
if ( isDefined( eventHandler ) ) if ( IsDefined( eventHandler ) )
{ {
client [[eventHandler]]( client.event ); client [[eventHandler]]( client.event );
LogDebug( "notify client for " + client.event.type );
client notify( level.eventTypes.localClientEvent, client.event );
} }
client.eventData = []; client.eventData = [];
@ -219,6 +236,95 @@ MonitorClientEvents()
// Helper Methods // 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 );
}
// 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 ) RequestClientMeta( metaKey )
{ {
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey ); getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
@ -299,24 +405,23 @@ SetClientMeta( metaKey, metaValue, clientId, direction )
SaveTrackingMetrics() SaveTrackingMetrics()
{ {
if ( level.iw4adminIntegrationDebug == 1 ) if ( !IsDefined( self.persistentClientId ) )
{ {
IPrintLn( "Saving tracking metrics for " + self.persistentClientId ); return;
} }
LogDebug( "Saving tracking metrics for " + self.persistentClientId );
if ( !IsDefined( self.lastShotCount ) ) if ( !IsDefined( self.lastShotCount ) )
{ {
self.lastShotCount = 0; self.lastShotCount = 0;
} }
currentShotCount = self getPlayerStat( "mostshotsfired" ); currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
change = currentShotCount - self.lastShotCount; change = currentShotCount - self.lastShotCount;
self.lastShotCount = currentShotCount; self.lastShotCount = currentShotCount;
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "Total Shots Fired increased by " + change );
{
IPrintLn( "Total Shots Fired increased by " + change );
}
if ( !IsDefined( change ) ) if ( !IsDefined( change ) )
{ {
@ -329,17 +434,16 @@ SaveTrackingMetrics()
} }
IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId ); IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
} }
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data ) BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
{ {
if ( !isDefined( data ) ) if ( !IsDefined( data ) )
{ {
data = ""; data = "";
} }
if ( !isDefined( eventSubtype ) ) if ( !IsDefined( eventSubtype ) )
{ {
eventSubtype = "None"; eventSubtype = "None";
} }
@ -380,10 +484,7 @@ MonitorBus()
{ {
continue; continue;
} }
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "-> " + eventString );
{
IPrintLn( "-> " + eventString );
}
NotifyClientEvent( strtok( eventString, ";" ) ); NotifyClientEvent( strtok( eventString, ";" ) );
@ -393,7 +494,7 @@ MonitorBus()
QueueEvent( request, eventType, notifyEntity ) QueueEvent( request, eventType, notifyEntity )
{ {
level endon( "disconnect" ); level endon( "game_ended" );
start = GetTime(); start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds maxWait = level.eventBus.timeout * 1000; // 30 seconds
@ -401,14 +502,11 @@ QueueEvent( request, eventType, notifyEntity )
while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait ) while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait )
{ {
level waittill_notify_or_timeout( "bus_ready", 1 ); level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 );
if ( GetDvar( level.eventBus.inVar ) != "" ) if ( GetDvar( level.eventBus.inVar ) != "" )
{ {
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "A request is already in progress..." );
{
IPrintLn( "A request is already in progress..." );
}
timedOut = "set"; timedOut = "set";
continue; continue;
} }
@ -418,41 +516,42 @@ QueueEvent( request, eventType, notifyEntity )
if ( timedOut == "set") if ( timedOut == "set")
{ {
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "Timed out waiting for response..." );
{
IPrintLn( "Timed out waiting for response..." );
}
if ( IsDefined( notifyEntity ) ) if ( IsDefined( notifyEntity ) )
{ {
notifyEntity NotifyClientEventTimeout( eventType ); notifyEntity NotifyClientEventTimeout( eventType );
} }
SetDvar( level.eventBus.inVar, "" );
return; return;
} }
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug("<- " + request );
{
IPrintLn("<- " + request);
}
SetDvar( level.eventBus.inVar, request ); SetDvar( level.eventBus.inVar, request );
} }
ParseDataString( data ) ParseDataString( data )
{ {
if ( !IsDefined( data ) )
{
LogDebug( "No data to parse" );
return [];
}
dataParts = strtok( data, "|" ); dataParts = strtok( data, "|" );
dict = []; dict = [];
counter = 0; for ( i = 0; i < dataParts.size; i++ )
foreach ( part in dataParts )
{ {
part = dataParts[i];
splitPart = strtok( part, "=" ); splitPart = strtok( part, "=" );
key = splitPart[0]; key = splitPart[0];
value = splitPart[1]; value = splitPart[1];
dict[key] = value; dict[key] = value;
dict[counter] = key; dict[i] = key;
counter++;
} }
return dict; return dict;
@ -479,17 +578,68 @@ NotifyClientEvent( eventInfo )
event.origin = origin; event.origin = origin;
event.target = target; event.target = target;
if ( level.iw4adminIntegrationDebug == 1 ) if ( IsDefined( event.data ) )
{ {
IPrintLn( "NotifyClientEvent->" + event.data ); LogDebug( "NotifyClientEvent->" + event.data );
} }
client = event.origin; if ( int( eventInfo[3] ) != -1 && !IsDefined( origin ) )
client.event = event; {
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 ); 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 // Event Handlers
///////////////////////////////// /////////////////////////////////
@ -501,17 +651,14 @@ OnClientDataReceived( event )
if ( event.subtype == "Fail" ) if ( event.subtype == "Fail" )
{ {
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "Received fail response" );
{
IPrintLn( "Received fail response" );
}
clientData.state = level.eventBus.failKey; clientData.state = level.eventBus.failKey;
return; return;
} }
if ( event.subtype == "Meta" ) if ( event.subtype == "Meta" )
{ {
if ( !isDefined( clientData.meta ) ) if ( !IsDefined( clientData.meta ) )
{ {
clientData.meta = []; clientData.meta = [];
} }
@ -519,12 +666,15 @@ OnClientDataReceived( event )
metaKey = event.data[0]; metaKey = event.data[0];
clientData.meta[metaKey] = event.data[metaKey]; clientData.meta[metaKey] = event.data[metaKey];
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
return; return;
} }
clientData.permissionLevel = event.data["level"]; clientData.permissionLevel = event.data["level"];
clientData.clientId = event.data["clientId"]; clientData.clientId = event.data["clientId"];
clientData.lastConnection = event.data["lastConnection"]; clientData.lastConnection = event.data["lastConnection"];
clientData.tag = event.data["tag"];
clientData.state = "complete"; clientData.state = "complete";
self.persistentClientId = event.data["clientId"]; self.persistentClientId = event.data["clientId"];
@ -536,49 +686,26 @@ OnExecuteCommand( event )
data = ParseDataString( event.data ); data = ParseDataString( event.data );
response = ""; response = "";
switch ( event.subtype ) command = level.clientCommandCallbacks[event.subtype];
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
executionContextEntity = event.origin;
if ( runAsTarget )
{ {
case "GiveWeapon": executionContextEntity = event.target;
response = event.target GiveWeaponImpl( data ); }
break;
case "TakeWeapons": if ( IsDefined( command ) )
response = event.target TakeWeaponsImpl();
break;
case "SwitchTeams":
response = event.target TeamSwitchImpl();
break;
case "Hide":
response = self HideImpl();
break;
case "Unhide":
response = self UnhideImpl();
break;
case "Alert":
response = event.target AlertImpl( data );
break;
case "Goto":
if ( IsDefined( event.target ) )
{ {
response = self GotoPlayerImpl( event.target ); response = executionContextEntity [[command]]( event, data );
} }
else else
{ {
response = self GotoImpl( data ); LogDebug( "Unknown Client command->" + event.subtype );
}
break;
case "Kill":
response = event.target KillImpl();
break;
case "NightMode":
NightModeImpl();
break;
case "SetSpectator":
response = event.target SetSpectatorImpl();
break;
} }
// send back the response to the origin, but only if they're not the target // send back the response to the origin, but only if they're not the target
if ( response != "" && IsPlayer( event.origin ) && event.origin != event.target ) if ( IsDefined( response ) && response != "" && IsPlayer( event.origin ) && event.origin != event.target )
{ {
event.origin IPrintLnBold( response ); event.origin IPrintLnBold( response );
} }
@ -587,213 +714,5 @@ OnExecuteCommand( event )
OnSetClientDataCompleted( event ) OnSetClientDataCompleted( event )
{ {
// IW4MAdmin let us know it persisted (success or fail) // IW4MAdmin let us know it persisted (success or fail)
if ( level.iw4adminIntegrationDebug == 1 ) LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
{
IPrintLn( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
}
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( 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.name + "^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;
}
HideImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
if ( !IsDefined( self.savedHealth ) || self.health < 1000 )
{
self.savedHealth = self.health;
self.savedMaxHealth = self.maxhealth;
}
self.maxhealth = 99999;
self.health = 99999;
self.isHidden = true;
self Hide();
self IPrintLnBold( "You are now ^5hidden ^7from other players" );
}
UnhideImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined( self.isHidden ) || !self.isHidden )
{
self IPrintLnBold( "You are not hidden" );
return;
}
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self.health = self.savedHealth;
self.maxhealth = self.savedMaxHealth;
self.isHidden = false;
self Show();
self IPrintLnBold( "You are now ^5visible ^7to other players" );
}
AlertImpl( data )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
return "Sent alert to " + self.name;
}
GotoImpl( 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 );
}
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;
}
NightModeImpl()
{
if ( !IsDefined ( level.nightModeEnabled ) )
{
level.nightModeEnabled = true;
}
else
{
level.nightModeEnabled = !level.nightModeEnabled;
}
message = "^5NightMode ^7is disabled";
if ( level.nightModeEnabled )
{
message = "^5NightMode ^7is enabled";
}
IPrintLnBold( message );
foreach( player in level.players )
{
player ToggleNightMode();
}
}
ToggleNightMode()
{
colorMap = 1;
fxDraw = 1;
if ( IsDefined( level.nightModeEnabled ) && level.nightModeEnabled )
{
colorMap = 0;
fxDraw = 0;
}
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "r_colorMap", colorMap );
self SetClientDvar( "fx_draw", fxDraw );
self SetClientDvar( "sv_cheats", 0 );
}
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,500 @@
#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["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\_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];
}
}
}
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,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";
}

16
GameFiles/README.MD Normal file
View File

@ -0,0 +1,16 @@
# Game Interface
Allows integration of IW4M-Admin to GSC, mainly used for special commands that need to use GSC in order to work.
But can also be used to read / write metadata from / to a profile and to get the player permission level.
## Installation Plutonium IW5
Move `_integration.gsc` to `%localappdata%\Plutonium\storage\iw5\scripts\`
## Installation IW4x
Move `_integration.gsc` to `IW4x/userraw/scripts`, `IW4x` being the root folder of your game server.

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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml
DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1 DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1
README.md = README.md README.md = README.md
version.txt = version.txt version.txt = version.txt
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1 DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
GameFiles\IW4x\userraw\scripts\_integration.gsc = GameFiles\IW4x\userraw\scripts\_integration.gsc
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"
@ -51,6 +49,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\ParserCSGOSM.js = Plugins\ScriptPlugins\ParserCSGOSM.js Plugins\ScriptPlugins\ParserCSGOSM.js = Plugins\ScriptPlugins\ParserCSGOSM.js
Plugins\ScriptPlugins\ParserPlutoniumT4COZM.js = Plugins\ScriptPlugins\ParserPlutoniumT4COZM.js Plugins\ScriptPlugins\ParserPlutoniumT4COZM.js = Plugins\ScriptPlugins\ParserPlutoniumT4COZM.js
Plugins\ScriptPlugins\GameInterface.js = Plugins\ScriptPlugins\GameInterface.js Plugins\ScriptPlugins\GameInterface.js = Plugins\ScriptPlugins\GameInterface.js
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
@ -67,6 +69,36 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Cod", "Integra
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Source", "Integrations\Source\Integrations.Source.csproj", "{9512295B-3045-40E0-9B7E-2409F2173E9D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Source", "Integrations\Source\Integrations.Source.csproj", "{9512295B-3045-40E0-9B7E-2409F2173E9D}"
EndProject 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
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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -371,6 +403,30 @@ Global
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU {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.ActiveCfg = Prerelease|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -385,6 +441,13 @@ Global
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F} {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F}
{A9348433-58C1-4B9C-8BB7-088B02529D9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B} {A9348433-58C1-4B9C-8BB7-088B02529D9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B}
{9512295B-3045-40E0-9B7E-2409F2173E9D} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

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