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
**/Master/static
**/Master/dev_env
/WebfrontCore/Views/Plugins/*
/WebfrontCore/wwwroot/**/dds
/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>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2037" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.5" />
<PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>
@ -63,6 +64,9 @@
<None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Resources\GeoLite2-Country.mmdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View File

@ -46,6 +46,8 @@ namespace IW4MAdmin.Application
public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; }
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
@ -57,10 +59,11 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
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 ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
@ -82,18 +85,19 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>();
ClientSvc = clientService;
PenaltySvc = penaltyService;
_alertManager = alertManager;
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_tokenSource = new CancellationTokenSource();
@ -508,6 +512,7 @@ namespace IW4MAdmin.Application
#endregion
_metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -587,16 +592,30 @@ namespace IW4MAdmin.Application
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();
IsRunning = false;
}
public void Restart()
{
IsRestartRequested = true;
Stop();
Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
}
[Obsolete]
@ -616,9 +635,9 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
}
public EFClient FindActiveClient(EFClient client) =>client.ClientNumber < 0 ?
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client :
.FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client;
public ClientService GetClientService()
@ -684,5 +703,6 @@ namespace IW4MAdmin.Application
}
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.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024;
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";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
_contextFactory = contextFactory;
_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)
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return;
}
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;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage()
var newMessage = new EFInboxMessage
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
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
{
context.Set<EFInboxMessage>().Add(newMessage);
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
}
}
}
}
}

View File

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

View File

@ -564,7 +564,56 @@
"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": [
{
@ -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",
"Maps": [

View File

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

View File

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

View File

@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -24,8 +25,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models;
using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient;
namespace IW4MAdmin
@ -73,7 +76,7 @@ namespace IW4MAdmin
{
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
if (client == null)
@ -96,6 +99,8 @@ namespace IW4MAdmin
client.ClientNumber = clientFromLog.ClientNumber;
client.Score = clientFromLog.Score;
client.Ping = clientFromLog.Ping;
client.Team = clientFromLog.Team;
client.TeamName = clientFromLog.TeamName;
client.CurrentServer = this;
client.State = ClientState.Connecting;
@ -114,7 +119,7 @@ namespace IW4MAdmin
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()))
{
@ -150,11 +155,9 @@ namespace IW4MAdmin
{
if (E.IsBlocking)
{
await E.Origin?.Lock();
await E.Origin.Lock();
}
bool canExecuteCommand = true;
try
{
if (!await ProcessEvent(E))
@ -162,53 +165,52 @@ namespace IW4MAdmin
return;
}
Command C = null;
Command command = null;
if (E.Type == GameEvent.EventType.Command)
{
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)
{
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 });
E.FailReason = GameEvent.EventFailReason.Invalid;
}
if (C != null)
if (command != null)
{
E.Extra = C;
E.Extra = command;
}
}
try
var canExecuteCommand = Manager.CommandInterceptors.All(interceptor =>
{
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
if (loginPlugin != null)
try
{
await loginPlugin.OnEventAsync(E, this);
return interceptor(E);
}
catch
{
return true;
}
});
if (!canExecuteCommand)
{
E.Origin.Tell(_translationLookup["SERVER_COMMANDS_INTERCEPTED"]);
}
catch (AuthorizationException e)
else if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd)
{
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
await command.ExecuteAsync(E);
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
E.Origin.ToString());
await cmd.ExecuteAsync(E);
}
var pluginTasks = Manager.Plugins
.Where(_plugin => _plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
@ -304,8 +306,16 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
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;
}
@ -316,7 +326,15 @@ namespace IW4MAdmin
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))
@ -353,9 +371,9 @@ namespace IW4MAdmin
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
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
@ -429,6 +447,7 @@ namespace IW4MAdmin
Clients[E.Origin.ClientNumber] = E.Origin;
try
{
E.Origin.GameName = (Reference.Game)GameName;
E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin;
}
@ -496,7 +515,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin);
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);
}
@ -506,7 +525,8 @@ namespace IW4MAdmin
{
Origin = E.Origin,
Target = E.Target,
Reason = E.Data
Reason = E.Data,
ReportedOn = DateTime.UtcNow
});
var newReport = new EFPenalty()
@ -569,10 +589,8 @@ namespace IW4MAdmin
Time = DateTime.UtcNow
});
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId,
Manager.CancellationToken);
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId,
Manager.CancellationToken);
await _metaService.SetPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin.ClientId);
await _metaService.SetPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin.ClientId);
}
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,
Message = message,
@ -669,23 +687,50 @@ namespace IW4MAdmin
else
{
Gametype = dict["gametype"];
Hostname = dict["hostname"];
if (dict.ContainsKey("gametype"))
{
Gametype = dict["gametype"];
}
string mapname = dict["mapname"] ?? CurrentMap.Name;
UpdateMap(mapname);
if (dict.ContainsKey("hostname"))
{
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
? dict["mapname"] ?? CurrentMap.Name
: CurrentMap.Name;
UpdateMap(newMapName);
}
}
else
{
var dict = (Dictionary<string, string>) E.Extra;
Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"];
MaxClients = int.Parse(dict["sv_maxclients"]);
var dict = (Dictionary<string, string>)E.Extra;
if (dict.ContainsKey("g_gametype"))
{
Gametype = dict["g_gametype"];
}
string mapname = dict["mapname"];
UpdateMap(mapname);
if (dict.ContainsKey("sv_hostname"))
{
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)
@ -721,6 +766,34 @@ namespace IW4MAdmin
{
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)
{
@ -743,7 +816,7 @@ namespace IW4MAdmin
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)
{
@ -788,10 +861,16 @@ namespace IW4MAdmin
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync()
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{
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();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -903,22 +982,16 @@ namespace IW4MAdmin
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
}
foreach (var plugin in Manager.Plugins)
{
await plugin.OnUnloadAsync();
}
}
DateTime start = DateTime.Now;
DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now;
private DateTime _lastMessageSent = DateTime.Now;
private DateTime _lastPlayerCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{
try
{
if (cts.IsCancellationRequested)
if (token.IsCancellationRequested)
{
await ShutdownInternal();
return true;
@ -926,17 +999,24 @@ namespace IW4MAdmin
try
{
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue && Utilities.IsDevelopment)
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue &&
Utilities.IsDevelopment)
{
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;
var e = new GameEvent()
var e = new GameEvent
{
Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient,
@ -949,23 +1029,20 @@ namespace IW4MAdmin
}
// 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;
var e = new GameEvent()
client.GameName = (Reference.Game)GameName;
var e = new GameEvent
{
Type = GameEvent.EventType.PreConnect,
Origin = client,
Owner = this,
IsBlocking = true,
Extra = client.GetAdditionalProperty<string>("BotGuid"),
Source = GameEvent.EventSource.Status
Source = GameEvent.EventSource.Status,
};
Manager.AddEvent(e);
@ -976,19 +1053,19 @@ namespace IW4MAdmin
foreach (var client in polledClients[2])
{
client.CurrentServer = this;
var e = new GameEvent()
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.Update,
Origin = client,
Owner = this
};
Manager.AddEvent(e);
Manager.AddEvent(gameEvent);
}
if (Throttled)
{
var _event = new GameEvent()
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.ConnectionRestored,
Owner = this,
@ -996,65 +1073,52 @@ namespace IW4MAdmin
Target = Utilities.IW4MAdminClient(this)
};
Manager.AddEvent(_event);
Manager.AddEvent(gameEvent);
}
LastPoll = DateTime.Now;
}
catch (NetworkException e)
catch (NetworkException ex)
{
if (!Throttled)
if (Throttled)
{
var _event = new GameEvent()
{
Type = GameEvent.EventType.ConnectionLost,
Owner = this,
Origin = Utilities.IW4MAdminClient(this),
Target = Utilities.IW4MAdminClient(this),
Extra = e,
Data = ConnectionErrors.ToString()
};
Manager.AddEvent(_event);
return true;
}
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.ConnectionLost,
Owner = this,
Origin = Utilities.IW4MAdminClient(this),
Target = Utilities.IW4MAdminClient(this),
Extra = ex,
Data = ConnectionErrors.ToString()
};
Manager.AddEvent(gameEvent);
return true;
}
finally
{
RunServerCollection();
}
if (DateTime.Now - _lastMessageSent <=
TimeSpan.FromSeconds(Manager.GetApplicationSettings().Configuration().AutoMessagePeriod) ||
BroadcastMessages.Count <= 0 || ClientNum <= 0)
{
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
if (LastMessage.TotalSeconds > Manager.GetApplicationSettings().Configuration().AutoMessagePeriod
&& BroadcastMessages.Count > 0
&& ClientNum > 0)
{
string[] messages = (await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(Environment.NewLine);
var messages =
(await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(
Environment.NewLine);
await BroadcastAsync(messages, token: Manager.CancellationToken);
foreach (string message in messages)
{
Broadcast(message);
}
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
start = DateTime.Now;
}
NextMessage = NextMessage == BroadcastMessages.Count - 1 ? 0 : NextMessage + 1;
_lastMessageSent = DateTime.Now;
return true;
}
@ -1066,26 +1130,56 @@ namespace IW4MAdmin
}
// this one is ok
catch (Exception e) when(e is ServerException || e is RConException)
catch (Exception e) when (e is ServerException || e is RConException)
{
using(LogContext.PushProperty("Server", ToString()))
using (LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogWarning(e, "Undesirable exception occured during processing updates");
}
return false;
}
catch (Exception e)
{
using(LogContext.PushProperty("Server", ToString()))
using (LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
}
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
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()
{
try
@ -1204,28 +1298,17 @@ namespace IW4MAdmin
this.GamePassword = gamePassword.Value;
UpdateMap(mapname);
if (RconParser.CanGenerateLogPath)
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
{
bool needsRestart = false;
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
needsRestart = true;
}
if (string.IsNullOrWhiteSpace(logfile.Value))
{
logfile.Value = "games_mp.log";
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
@ -1431,6 +1514,11 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
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)
{
@ -1461,6 +1549,11 @@ namespace IW4MAdmin
activeClient.SetLevel(Permission.Banned, originClient);
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)
{
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());
targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress);
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty);
}

View File

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

View File

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

View File

@ -11,6 +11,7 @@ using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
@ -19,13 +20,14 @@ namespace IW4MAdmin.Application.Meta
/// implementation of IResourceQueryHelper
/// used to pull in penalties applied to a given client id
/// </summary>
public class ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
public class
ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger,
public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{
_contextFactory = contextFactory;
@ -33,28 +35,36 @@ namespace IW4MAdmin.Application.Meta
_appConfig = appConfig;
}
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(
ClientPaginationRequest query)
{
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId)
.Select(_client => new {_client.AliasLinkId, _client.CurrentAliasId })
.FirstOrDefaultAsync();
.Where(_client => _client.ClientId == query.ClientId)
.Select(_client => new { _client.AliasLinkId, _client.CurrentAliasId })
.FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => _penalty.OffenderId == query.ClientId ||
linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId);
IQueryable<EFPenalty> iqIpLinkedPenalties = null;
IQueryable<EFPenalty> identifierPenalties = null;
if (!_appConfig.EnableImplicitAccountLinking)
{
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();
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))
.Select(alias => alias.LinkId)
.ToListAsync();
@ -65,13 +75,18 @@ namespace IW4MAdmin.Application.Meta
}
var iqAllPenalties = iqPenalties;
if (iqIpLinkedPenalties != null)
{
iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties);
}
var penalties = await iqAllPenalties
if (identifierPenalties != null)
{
iqAllPenalties = iqPenalties.Union(identifierPenalties);
}
var penalties = await iqAllPenalties
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When)
.Take(query.Count)
@ -97,7 +112,7 @@ namespace IW4MAdmin.Application.Meta
{
// todo: maybe actually 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(config.NoticeLineSeparator);
// 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)
{
@ -117,4 +117,4 @@ namespace IW4MAdmin.Application.Misc
return segments;
}
}
}
}

View File

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

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.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
@ -19,13 +22,15 @@ public class MetaServiceV2 : IMetaServiceV2
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory)
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
_serviceProvider = serviceProvider;
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
}
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,
@ -207,7 +232,7 @@ public class MetaServiceV2 : IMetaServiceV2
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;
}
@ -439,61 +464,14 @@ public class MetaServiceV2 : IMetaServiceV2
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;
}
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
{
var metaList = meta.ToList();
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;
return meta;
}
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 readonly string _fileName;
private readonly SemaphoreSlim _onProcessing = new(1, 1);
private readonly SemaphoreSlim _onDvarActionComplete = new(1, 1);
private bool _successfullyLoaded;
private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
@ -116,7 +117,8 @@ namespace IW4MAdmin.Application.Misc
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));
@ -276,8 +278,8 @@ namespace IW4MAdmin.Application.Misc
{
_logger.LogDebug("OnLoad executing for {Name}", Name);
_scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", GetDvarAsync);
_scriptEngine.SetValue("setDvar", SetDvarAsync);
_scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask;
@ -337,13 +339,49 @@ namespace IW4MAdmin.Application.Misc
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>
/// finds declared script commands in the script plugin
/// </summary>
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
IScriptCommandFactory scriptCommandFactory)
{
var commandList = new List<IManagerCommand>();
@ -410,7 +448,7 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.SetValue("_event", gameEvent);
var jsEventObject = _scriptEngine.Evaluate("_event");
dynamicCommand.execute.Target.Invoke(_scriptEngine, jsEventObject);
}
@ -424,7 +462,7 @@ namespace IW4MAdmin.Application.Misc
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
@ -454,83 +492,133 @@ namespace IW4MAdmin.Application.Misc
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
{
result = (await server.GetDvarAsync<string>(dvarName, token: tokenSource.Token)).Value;
}
catch
{
success = false;
}
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
await _onProcessing.WaitAsync();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, result),
JsValue.FromObject(_scriptEngine, success),
});
}
_logger.LogDebug("Waiting for onDvarActionComplete -> get");
_onDvarActionComplete.Wait();
_logger.LogDebug("Completed wait for onDvarActionComplete -> get");
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release();
}
}
});
}
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
Task.Run<Task>(async () =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var success = true;
try
{
await server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token);
}
catch
{
success = false;
}
await _onProcessing.WaitAsync();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, dvarValue),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, value),
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
{
if (_onProcessing.CurrentCount == 0)
OnComplete(result);
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
});
}, tokenSource.Token);
}
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
void OnComplete(IAsyncResult result)
{
try
{
var success = (bool)result.AsyncState;
_logger.LogDebug("Waiting for onDvarActionComplete -> set");
_onDvarActionComplete.Wait();
_logger.LogDebug("Completed wait for onDvarActionComplete -> set");
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, dvarValue),
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
{
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();
}
}
}, tokenSource.Token);
}
}

View File

@ -111,7 +111,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
return;
}
_logger.LogDebug("-Releasing OnTick for timer");
_onDependentAction?.Release(1);
}
private void OnTick()
@ -128,7 +128,8 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_onRunningTick.Reset();
// 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;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_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,
ServerId = await server.GetIdForServer(),
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
ClientCount = server.ClientNum
ClientCount = server.ClientNum,
ConnectionInterrupted = server.Throttled,
}));
return data;
@ -144,4 +145,4 @@ namespace IW4MAdmin.Application.Misc
context.SaveChanges();
}
}
}
}

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
}
public async Task<(int?, DateTime?)>
@ -135,7 +138,9 @@ namespace IW4MAdmin.Application.Misc
{
snapshot.ServerId,
snapshot.CapturedAt,
snapshot.ClientCount
snapshot.ClientCount,
snapshot.ConnectionInterrupted,
MapName = snapshot.Map.Name,
})
.OrderBy(snapshot => snapshot.CapturedAt)
.ToListAsync(cancellationToken);
@ -143,8 +148,8 @@ namespace IW4MAdmin.Application.Misc
return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
{
ServerId = byServer.Key,
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot()
{Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList()
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot
{ Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount, ConnectionInterrupted = snapshot.ConnectionInterrupted ?? false, Map = snapshot.MapName}).ToList()
}).ToList();
}, nameof(_clientHistoryCache), TimeSpan.MaxValue);
@ -158,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
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
{
private readonly ConcurrentDictionary<long, TokenState> _tokens;
private readonly ConcurrentDictionary<int, TokenState> _tokens;
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;
public TokenAuthentication()
{
_tokens = new ConcurrentDictionary<long, TokenState>();
_tokens = new ConcurrentDictionary<int, TokenState>();
_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)
{
_tokens.TryRemove(networkId, out _);
_tokens.TryRemove(authInfo.ClientId, out _);
}
return authorizeSuccessful;
}
public TokenState GenerateNextToken(long networkId)
public TokenState GenerateNextToken(ITokenIdentifier authInfo)
{
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
@ -53,17 +54,16 @@ namespace IW4MAdmin.Application.Misc
state = new TokenState
{
NetworkId = networkId,
Token = _generateToken(),
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
foreach (var (key, value) in _tokens)
{
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod)
if (DateTime.Now - value.RequestTime > TimeoutPeriod)
{
_tokens.TryRemove(key, out _);
}

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -19,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
public class BaseRConParser : IRConParser
{
private readonly ILogger _logger;
private static string _botIpIndicator = "00000000.";
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.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)?$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -80,6 +82,7 @@ namespace IW4MAdmin.Application.RConParsers
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);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
}
@ -102,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
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);
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)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
@ -149,16 +176,16 @@ namespace IW4MAdmin.Application.RConParsers
return new StatusResponse
{
Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern)
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus)
};
}
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, string groupPattern)
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, ParserRegex parserRegex)
{
if (string.IsNullOrEmpty(groupPattern))
if (string.IsNullOrEmpty(parserRegex.Pattern))
{
return default;
}
@ -166,10 +193,10 @@ namespace IW4MAdmin.Application.RConParsers
string value = null;
foreach (var line in response)
{
var regex = Regex.Match(line, groupPattern);
if (regex.Success)
var regex = Regex.Match(line, parserRegex.Pattern);
if (regex.Success && parserRegex.GroupMapping.ContainsKey(groupType))
{
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
}
}
@ -195,6 +222,31 @@ namespace IW4MAdmin.Application.RConParsers
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)
{
List<EFClient> StatusPlayers = new List<EFClient>();
@ -239,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
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
{
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
@ -255,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
continue;
}
var client = new EFClient()
var client = new EFClient
{
CurrentAlias = new EFAlias()
CurrentAlias = new EFAlias
{
Name = name,
IPAddress = ip
@ -309,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default;
public TimeSpan OverrideTimeoutForCommand(string command)
public TimeSpan? OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
if (string.IsNullOrEmpty(command))
{
return TimeSpan.FromSeconds(30);
return TimeSpan.Zero;
}
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
}
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 IDictionary<string, string> OverrideDvarNameMapping { 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 NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public int? DefaultRConPort { get; set; }
public string DefaultInstallationDirectoryHint { get; set; }
public short FloodProtectInterval { get; set; } = 750;
public bool ShouldRemoveDiacritics { get; set; }
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
{
@ -46,7 +48,7 @@ namespace IW4MAdmin.Application.RConParsers
{ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), ":^"},
{ColorCodes.Wildcard.ToString(), "^:"}
};
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
@ -58,6 +60,25 @@ namespace IW4MAdmin.Application.RConParsers
StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = 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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@ -9,6 +10,11 @@ namespace Data.Abstractions
{
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
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, object id = null, CancellationToken token = default);
}
}
}

View File

@ -85,7 +85,15 @@ namespace Data.Context
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 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 =>
{
@ -120,6 +128,9 @@ namespace Data.Context
ent.Property(_alias => _alias.SearchableName).HasMaxLength(24);
ent.HasIndex(_alias => _alias.SearchableName);
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 =>

View File

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

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
@ -15,8 +17,8 @@ namespace Data.Helpers
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
new ConcurrentDictionary<string, CacheState<TReturnType>>();
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
private readonly object _defaultKey = new();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15;
@ -51,41 +53,61 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{
if (_cacheStates.ContainsKey(key))
{
_logger.LogDebug("Cache key {Key} is already added", key);
return;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{
ids ??= new[] { _defaultKey };
if (!_cacheStates.ContainsKey(key))
{
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
foreach (var id in ids)
{
if (_cacheStates[key].ContainsKey(id))
{
continue;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
}
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))
{
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 on, we want to only check the value, because it'll be refreshed automatically
@ -115,4 +137,4 @@ namespace Data.Helpers
}
}
}
}
}

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")
.HasColumnType("datetime(6)");
b.Property<int>("GameName")
.HasColumnType("int");
b.Property<DateTime>("LastConnection")
.HasColumnType("datetime(6)");
@ -87,12 +90,13 @@ namespace Data.Migrations.MySql
b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
});
@ -453,6 +457,8 @@ namespace Data.Migrations.MySql
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking");
b.HasIndex("ServerId");
@ -804,6 +810,11 @@ namespace Data.Migrations.MySql
.HasMaxLength(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")
.HasMaxLength(24)
.HasColumnType("varchar(24)");
@ -816,6 +827,8 @@ namespace Data.Migrations.MySql
b.HasIndex("Name");
b.HasIndex("SearchableIPAddress");
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress");
@ -1081,6 +1094,9 @@ namespace Data.Migrations.MySql
b.Property<int>("ClientCount")
.HasColumnType("int");
b.Property<bool?>("ConnectionInterrupted")
.HasColumnType("tinyint(1)");
b.Property<int>("MapId")
.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")
.HasColumnType("timestamp without time zone");
b.Property<int>("GameName")
.HasColumnType("integer");
b.Property<DateTime>("LastConnection")
.HasColumnType("timestamp without time zone");
@ -94,12 +97,13 @@ namespace Data.Migrations.Postgresql
b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
});
@ -472,6 +476,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking");
b.HasIndex("ServerId");
@ -843,6 +849,11 @@ namespace Data.Migrations.Postgresql
.HasMaxLength(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")
.HasMaxLength(24)
.HasColumnType("character varying(24)");
@ -855,6 +866,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("Name");
b.HasIndex("SearchableIPAddress");
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress");
@ -1134,6 +1147,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("ClientCount")
.HasColumnType("integer");
b.Property<bool?>("ConnectionInterrupted")
.HasColumnType("boolean");
b.Property<int>("MapId")
.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")
.HasColumnType("TEXT");
b.Property<int>("GameName")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastConnection")
.HasColumnType("TEXT");
@ -85,12 +88,13 @@ namespace Data.Migrations.Sqlite
b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
});
@ -451,6 +455,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking");
b.HasIndex("ServerId");
@ -802,6 +808,11 @@ namespace Data.Migrations.Sqlite
.HasMaxLength(24)
.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")
.HasMaxLength(24)
.HasColumnType("TEXT");
@ -814,6 +825,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("Name");
b.HasIndex("SearchableIPAddress");
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress");
@ -1079,6 +1092,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("ClientCount")
.HasColumnType("INTEGER");
b.Property<bool?>("ConnectionInterrupted")
.HasColumnType("INTEGER");
b.Property<int>("MapId")
.HasColumnType("INTEGER");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,8 +29,9 @@ onPlayerConnect( player )
for( ;; )
{
level waittill( "connected", player );
player setClientDvar("cl_autorecord", 1);
player setClientDvar("cl_demosKeep", 200);
player setClientDvars( "cl_autorecord", 1,
"cl_demosKeep", 200 );
player thread waitForFrameThread();
player thread waitForAttack();
}
@ -60,7 +61,7 @@ getHttpString( url )
runRadarUpdates()
{
interval = int(getDvar("sv_printradar_updateinterval"));
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; )
{
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++;
}
lastAttack = int(getTime()) - int(self.lastAttackTime);
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
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()
{
interval = int(getDvar("sv_printradar_updateinterval"));
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; )
{
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++;
}
lastAttack = int(getTime()) - int(self.lastAttackTime);
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
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",
"Strain",
"Snap"
]
```
"Snap",
"Strain"
]
```

View File

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

View File

@ -1,10 +1,16 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include maps\mp\gametypes\_playerlogic;
init()
Init()
{
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// setup default vars
level.eventBus = spawnstruct();
level.eventBus.inVar = "sv_iw4madmin_in";
@ -13,6 +19,13 @@ init()
level.eventBus.timeoutKey = "timeout";
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.eventTypes = spawnstruct();
@ -22,17 +35,34 @@ init()
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
SetDvarIfUninitialized( level.eventBus.inVar, "" );
SetDvarIfUninitialized( level.eventBus.outVar, "" );
SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
level.iw4madminIntegrationDebug = 0;
// map the event type to the handler
level.eventCallbacks = [];
level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived;
level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand;
level.eventCallbacks[level.eventTypes.setClientDataCompleted] = ::OnSetClientDataCompleted;
level.clientCommandCallbacks = [];
level.clientCommandRusAsTarget = [];
level.logger = spawnstruct();
level.overrideMethods = [];
level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
InitializeLogger();
wait ( 0.05 ); // needed to give script engine time to propagate notifies
level notify( level.notifyTypes.integrationBootstrapInitialized );
level waittill( level.notifyTypes.gameFunctionsInitialized );
LogDebug( "Integration received notify that game functions are initialized" );
_SetDvarIfUninitialized( level.eventBus.inVar, "" );
_SetDvarIfUninitialized( level.eventBus.outVar, "" );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
@ -51,15 +81,19 @@ init()
OnPlayerConnect()
{
level endon ( "disconnect" );
level endon ( "game_ended" );
for ( ;; )
{
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();
}
@ -68,12 +102,6 @@ OnPlayerConnect()
player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval();
// only toggle if it's enabled
if ( IsDefined( level.nightModeEnabled ) && level.nightModeEnabled )
{
player ToggleNightMode();
}
}
}
@ -84,13 +112,13 @@ OnPlayerSpawned()
for ( ;; )
{
self waittill( "spawned_player" );
self PlayerConnectEvents();
self PlayerSpawnEvents();
}
}
OnPlayerDisconnect()
{
level endon ( "disconnect" );
self endon ( "disconnect" );
for ( ;; )
{
@ -101,32 +129,30 @@ OnPlayerDisconnect()
OnPlayerJoinedTeam()
{
self endon( "disconnect" );
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_team" );
for( ;; )
{
self waittill( "joined_team" );
// join spec and join team occur at the same moment - out of order logging would be problematic
wait( 0.25 );
LogPrint( GenerateJoinTeamString( false ) );
}
}
}
OnPlayerJoinedSpectators()
{
self endon( "disconnect" );
self endon( "disconnect" );
for( ;; )
{
for( ;; )
{
self waittill( "joined_spectators" );
LogPrint( GenerateJoinTeamString( true ) );
}
}
}
OnGameEnded()
{
level endon ( "disconnect" );
for ( ;; )
{
level waittill( "game_ended" );
@ -151,29 +177,22 @@ DisplayWelcomeData()
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
PlayerConnectEvents()
PlayerSpawnEvents()
{
self endon( "disconnect" );
if ( IsDefined( self.isHidden ) && self.isHidden )
{
self HideImpl();
}
clientData = self.pers[level.clientDataKey];
// this gives IW4MAdmin some time to register the player before making the request;
// although probably not necessary some users might have a slow database or poll rate
wait ( 2 );
if ( isDefined( clientData.state ) && clientData.state == "complete" )
if ( IsDefined( clientData.state ) && clientData.state == "complete" )
{
return;
}
self RequestClientBasicData();
// example of requesting meta from IW4MAdmin
// self RequestClientMeta( "LastServerPlayed" );
}
PlayerTrackingOnInterval()
@ -192,23 +211,21 @@ PlayerTrackingOnInterval()
MonitorClientEvents()
{
level endon( "disconnect" );
self endon( "disconnect" );
level endon( "game_ended" );
for ( ;; )
{
level waittill( level.eventTypes.localClientEvent, client );
if ( level.iw4adminIntegrationDebug == 1 )
{
self IPrintLn( "Processing Event " + client.event.type + "-" + client.event.subtype );
}
LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
eventHandler = level.eventCallbacks[client.event.type];
if ( isDefined( eventHandler ) )
if ( IsDefined( eventHandler ) )
{
client [[eventHandler]]( client.event );
LogDebug( "notify client for " + client.event.type );
client notify( level.eventTypes.localClientEvent, client.event );
}
client.eventData = [];
@ -219,6 +236,95 @@ MonitorClientEvents()
// 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 )
{
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
@ -299,24 +405,23 @@ SetClientMeta( metaKey, metaValue, clientId, direction )
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 ) )
{
self.lastShotCount = 0;
}
currentShotCount = self getPlayerStat( "mostshotsfired" );
currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
change = currentShotCount - self.lastShotCount;
self.lastShotCount = currentShotCount;
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Total Shots Fired increased by " + change );
}
LogDebug( "Total Shots Fired increased by " + change );
if ( !IsDefined( change ) )
{
@ -329,17 +434,16 @@ SaveTrackingMetrics()
}
IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
}
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
{
if ( !isDefined( data ) )
if ( !IsDefined( data ) )
{
data = "";
}
if ( !isDefined( eventSubtype ) )
if ( !IsDefined( eventSubtype ) )
{
eventSubtype = "None";
}
@ -363,7 +467,7 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
MonitorBus()
{
level endon( "game_ended" );
for( ;; )
{
wait ( 0.1 );
@ -380,10 +484,7 @@ MonitorBus()
{
continue;
}
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "-> " + eventString );
}
LogDebug( "-> " + eventString );
NotifyClientEvent( strtok( eventString, ";" ) );
@ -393,7 +494,7 @@ MonitorBus()
QueueEvent( request, eventType, notifyEntity )
{
level endon( "disconnect" );
level endon( "game_ended" );
start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds
@ -401,14 +502,11 @@ QueueEvent( request, eventType, notifyEntity )
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 ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "A request is already in progress..." );
}
LogDebug( "A request is already in progress..." );
timedOut = "set";
continue;
}
@ -418,41 +516,42 @@ QueueEvent( request, eventType, notifyEntity )
if ( timedOut == "set")
{
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Timed out waiting for response..." );
}
LogDebug( "Timed out waiting for response..." );
if ( IsDefined( notifyEntity) )
if ( IsDefined( notifyEntity ) )
{
notifyEntity NotifyClientEventTimeout( eventType );
}
SetDvar( level.eventBus.inVar, "" );
return;
}
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn("<- " + request);
}
LogDebug("<- " + request );
SetDvar( level.eventBus.inVar, request );
}
ParseDataString( data )
{
if ( !IsDefined( data ) )
{
LogDebug( "No data to parse" );
return [];
}
dataParts = strtok( data, "|" );
dict = [];
counter = 0;
foreach ( part in dataParts )
for ( i = 0; i < dataParts.size; i++ )
{
part = dataParts[i];
splitPart = strtok( part, "=" );
key = splitPart[0];
value = splitPart[1];
dict[key] = value;
dict[counter] = key;
counter++;
dict[i] = key;
}
return dict;
@ -479,17 +578,68 @@ NotifyClientEvent( eventInfo )
event.origin = origin;
event.target = target;
if ( level.iw4adminIntegrationDebug == 1 )
if ( IsDefined( event.data ) )
{
IPrintLn( "NotifyClientEvent->" + event.data );
LogDebug( "NotifyClientEvent->" + event.data );
}
if ( int( eventInfo[3] ) != -1 && !IsDefined( origin ) )
{
LogDebug( "origin is null but the slot id is " + int( eventInfo[3] ) );
}
if ( int( eventInfo[4] ) != -1 && !IsDefined( target ) )
{
LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) );
}
client = event.origin;
if ( IsDefined( target ) )
{
client = event.target;
}
else if ( IsDefined( origin ) )
{
client = event.origin;
}
else
{
LogDebug( "Neither origin or target are set but we are a Client Event, aborting" );
return;
}
client.event = event;
level notify( level.eventTypes.localClientEvent, client );
}
GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
for ( i = 0; i < level.players.size; i++ )
{
if ( level.players[i] getEntityNumber() == clientNum )
{
return level.players[i];
}
}
return undefined;
}
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
{
if ( IsDefined( level.clientCommandCallbacks[commandName] ) && IsDefined( shouldOverwrite ) && !shouldOverwrite )
{
return;
}
level.clientCommandCallbacks[commandName] = callback;
level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean
}
//////////////////////////////////
// Event Handlers
/////////////////////////////////
@ -501,23 +651,22 @@ OnClientDataReceived( event )
if ( event.subtype == "Fail" )
{
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Received fail response" );
}
LogDebug( "Received fail response" );
clientData.state = level.eventBus.failKey;
return;
}
if ( event.subtype == "Meta" )
{
if ( !isDefined( clientData.meta ) )
if ( !IsDefined( clientData.meta ) )
{
clientData.meta = [];
}
metaKey = event.data[0];
clientData.meta[metaKey] = event.data[metaKey];
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
return;
}
@ -525,6 +674,7 @@ OnClientDataReceived( event )
clientData.permissionLevel = event.data["level"];
clientData.clientId = event.data["clientId"];
clientData.lastConnection = event.data["lastConnection"];
clientData.tag = event.data["tag"];
clientData.state = "complete";
self.persistentClientId = event.data["clientId"];
@ -535,50 +685,27 @@ OnExecuteCommand( event )
{
data = ParseDataString( event.data );
response = "";
switch ( event.subtype )
command = level.clientCommandCallbacks[event.subtype];
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
executionContextEntity = event.origin;
if ( runAsTarget )
{
case "GiveWeapon":
response = event.target GiveWeaponImpl( data );
break;
case "TakeWeapons":
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 );
}
else
{
response = self GotoImpl( data );
}
break;
case "Kill":
response = event.target KillImpl();
break;
case "NightMode":
NightModeImpl();
break;
case "SetSpectator":
response = event.target SetSpectatorImpl();
break;
executionContextEntity = event.target;
}
if ( IsDefined( command ) )
{
response = executionContextEntity [[command]]( event, data );
}
else
{
LogDebug( "Unknown Client command->" + event.subtype );
}
// send back the response to the origin, but only if they're not the target
if ( response != "" && IsPlayer( event.origin ) && event.origin != event.target )
if ( IsDefined( response ) && response != "" && IsPlayer( event.origin ) && event.origin != event.target )
{
event.origin IPrintLnBold( response );
}
@ -587,213 +714,5 @@ OnExecuteCommand( event )
OnSetClientDataCompleted( event )
{
// IW4MAdmin let us know it persisted (success or fail)
if ( level.iw4adminIntegrationDebug == 1 )
{
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";
LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
}

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

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