Compare commits

..

1078 Commits

Author SHA1 Message Date
c550d424dd fix startup issue with no config 2023-04-09 09:59:10 -05:00
f4ded4cc1f fix profanity determent on chat enabled check 2023-04-08 16:11:22 -05:00
d8c0cd47f5 server banner tweaks 2023-04-08 15:43:47 -05:00
1f77d10eed fix extra IP lookups in server banner plugin 2023-04-08 12:00:28 -05:00
222f2ba5f8 add ServerBanner.js to solution 2023-04-08 10:10:56 -05:00
8c48151ab6 add server banner plugin for iframe embeds 2023-04-08 10:10:15 -05:00
c5a283a02e improve login plugin structure and fix load issue 2023-04-08 09:43:33 -05:00
d0911b7b8a add server game group collapse to advanced stats 2023-04-07 21:38:41 -05:00
388434133b fix issue with profanity plugin enabled check and add KickOnInfringingName setting 2023-04-07 21:21:18 -05:00
6bb97c7d83 Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2023-04-07 20:53:25 -05:00
Edo
c348283c94 fix iw4x, integration. improve scripts overall (#287)
* fix(scripts): correct usage of notifyOnPlayerCommand

* fix(scripts): correct iw4x usage of is bot

* fix(scripts): correct iw4x usage of is bot

* fix(scripts): fix noclip on iw4x

* fix(scripts): ident

* iw5 too
2023-04-07 20:42:18 -05:00
HGM
a434420951 Added Zombie Game Modes + Bonus & Zombie Maps for T7/BOIII (#286)
* Added Zombie Game Modes

Added Zombie Game Modes for T4, T6 & BOIII

* Added Bonus Maps & Zombies (T7/BOIII)

Added Missing Bonus Maps & Zombies maps for T7/BOIII
2023-04-07 20:41:25 -05:00
19bbdede45 Add CoD 4's missing gametypes. (#280)
* Add CoD 4's missing gametypes.

* Fixed invalid issue
2023-04-07 20:41:12 -05:00
129e70c82c Add grouping for servers on top stats, live radar, and scoreboard 2023-04-07 16:23:24 -05:00
c6c7ca6305 enable support for custom say name on non IW4 games with tell/say raw 2023-04-07 14:04:04 -05:00
12ddb87fc2 remove unnecessary separator on client profile 2023-04-06 21:19:08 -05:00
bc0ec6c050 track private slots for webfront overview 2023-04-05 23:10:40 -05:00
99e0990770 update script helper method name 2023-04-05 22:27:48 -05:00
af2925287d Add NotifyAfterDelay helper method 2023-04-05 22:26:42 -05:00
ffb32ccc45 add back missing "Port" field for Server 2023-04-05 22:26:04 -05:00
e558d912cf Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2023-04-05 14:15:46 -05:00
2e6a1efb47 fix issue with BanBroadcasting 2023-04-05 14:12:59 -05:00
4442826bcf misc clearnup 2023-04-05 10:16:11 -05:00
6db1f6db07 update plugin references to newest shared library 2023-04-05 10:15:36 -05:00
d9d5a56ab0 update stats plugin for server caching and better DI usage 2023-04-05 10:15:10 -05:00
f41ce39180 implement new eventing system 2023-04-05 09:54:57 -05:00
2e726ea9ed update references from IP to ListenAddress 2023-04-04 22:21:18 -05:00
6fa172d757 update controllers to use DI stat manager 2023-04-04 22:10:37 -05:00
da54c5d327 refactor BaseEventParser to utilize new event system 2023-04-04 21:54:41 -05:00
fb82cbe6f2 small tweak to restart and runas command 2023-04-04 21:53:51 -05:00
5f5c0f1cfb improve threading synchronization for date lookup cache 2023-04-04 21:53:01 -05:00
5f5fb8230e remove unneeded classes 2023-04-04 21:45:33 -05:00
51fae05a73 add configuration watcher implementation 2023-04-04 21:44:08 -05:00
c14042a109 improve threading synchronization for BaseConfigurationHandlers 2023-04-04 21:42:17 -05:00
fab3cf95d6 implement PluginV2 for script plugins 2023-04-04 18:24:13 -05:00
ad20572879 update readmessage command to use TellAsync 2023-04-03 15:56:13 -05:00
3364473ce2 update help command to use TellAsync 2023-04-03 15:55:46 -05:00
HGM
710382d432 Update DefaultSettings.json (#282)
Update IW4x Map Names for "Modern Warfare 3 DLC Pack"
2023-03-23 13:04:09 -05:00
Edo
b258d51863 fix(boiii): workaround the goofiest bug (#284)
* fix(boiii): workaround the goofiest bug
2023-03-23 13:03:54 -05:00
782201b086 feature(script_plugins): boiii parser 2023-03-16 10:48:14 -05:00
676589a3e0 fix threading issue with alert manager 2023-02-17 14:37:52 -06:00
6c9ac1f7bb update mysql migration to add explicit length for searchable ip 2023-02-13 08:24:45 -06:00
e8bdde70fb implement IConfigurationHandlerV2 2023-02-11 21:09:02 -06:00
dab429776d define new event types 2023-02-11 21:03:35 -06:00
5e32536821 update vpn detection to script plugin v2 2023-02-11 21:02:20 -06:00
59e3813fa7 update action on report to script plugin v2 2023-02-11 21:01:47 -06:00
66c0561e7f update stats plugin to IPluginV2 2023-02-11 21:01:28 -06:00
7b8f6421aa update welcome plugin to IPluginV2 2023-02-11 20:56:52 -06:00
4ba56b53a4 update profanity determent plugin to IPluginV2 2023-02-11 20:49:21 -06:00
a50e61318c update mute plugin to IPluginV2 2023-02-11 20:48:31 -06:00
83207b4b40 update login plugin to IPluginV2 2023-02-11 20:46:57 -06:00
ba9e393363 update live radar plugin to IPluginV2 2023-02-11 20:46:08 -06:00
2688790736 update auto message feed plugin to IPluginV2 2023-02-11 20:44:04 -06:00
8fc47ec6c4 fix edge case for temp mute penalties with no expiration 2023-01-24 14:43:00 -06:00
12e3fd9238 fix permissions issue with search 2023-01-24 14:32:48 -06:00
6edf3f1ae9 fix issue with default date and default order on advanced search 2023-01-23 21:23:02 -06:00
8f20a2e2cd add index to last connection for improved search speed 2023-01-23 21:10:33 -06:00
b002991686 update BuildWebCompiler to support newer SCSS functions 2023-01-23 18:33:46 -06:00
ba40478d11 add "advanced" search functionality 2023-01-23 16:38:16 -06:00
c89314667c Update IW5 gametype names. (#240) 2023-01-11 09:05:34 -06:00
b14d7b6865 remove reference to deprecated httpGet in customcallbacks 2023-01-09 13:44:41 -06:00
dabad54872 add more ported iw4x maps to default settings 2023-01-06 18:03:17 -06:00
74e792bfdc merge 2023-01-06 13:45:25 -06:00
eac8483885 temporarily disable plugin interactions 2023-01-06 13:42:38 -06:00
0ebd582532 add new ported cod4 maps to iw4x map list 2023-01-06 13:39:15 -06:00
31e3e98d06 update h1 parser for chat localization 2023-01-06 12:25:24 -06:00
9ef189d303 update iw6x chat localize text 2023-01-06 10:49:10 -06:00
ae101f1c3a fix for iw6x and s1x parser 2023-01-05 21:43:57 -06:00
ef5e36b224 add game name to dropdown list on web console 2022-12-22 19:37:56 -06:00
d0f72390fb fix hidden text for password protected servers on chat context 2022-12-22 19:28:59 -06:00
Edo
8bcc9354fd Update H1 parser too based on iw6&s1 experience 2022-11-07 08:48:38 -06:00
0674ef800b fix(ParserIW6x): Filter out say/say_team correctly 2022-11-07 08:48:38 -06:00
1349cf84b7 properly set the localize text char for s1x parser 2022-11-03 20:11:05 -05:00
b311ecefc2 feature(parser): Option to override special localize character 2022-11-03 20:05:59 -05:00
16739ce455 misc fixes 2022-10-25 15:39:49 -05:00
b5b01cba4c improve webfront command error feedback 2022-10-25 14:52:12 -05:00
797642f3e6 only titleize single word titles on action dialogs 2022-10-25 14:03:35 -05:00
6fa15d3dcc don't intercept commands for login plugin if they are from webfront 2022-10-25 13:22:33 -05:00
7d6bf88bfd Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-10-24 21:15:29 -05:00
2e149ddafd fix profile issue with no available interactions 2022-10-24 21:11:00 -05:00
a16986f7a3 Mute Banner for Profile & Prevent Self-Target & Correctly Expire Early Unmutes (#272)
* Fix self-targeting
Remove creation of penalty on mute expiration

* Display mute penalties on profile
Expire mute penalties on unmute

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

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

* Display mute penalties on profile
Expire mute penalties on unmute

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

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

* Change order of operation

* Added MuteInfoCommand.cs

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

Makes date readable for target player

Resolved translation string inconsistencies

Minor code cleanups

Initial commit from review

Cleaned up code & amended a few checks

Comment typo

Fix infinite unmuting

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

Committing before refactor

Refactor from review

Removed reference to AdditionalProperty

Fix check for meta state when unmuting

Continued request solves main problem

Handle potential failed command execution

Missed CommandExecuted onJoin

Fix another PS Reference to Invoke-WebRequest

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

Comparing

Revert formatting changes

Removing MuteList for Penalty
Added Mute, TempMute & Unmute Penalty

Fixed reference in Mute.csproj & Removed ListMutesCommand.cs
2022-10-13 13:29:39 -05:00
a15da15d3e fix issue with vpn detection using new interaction 2022-10-13 10:47:25 -05:00
3b83729457 add level color coding to target on penalty list for issue #265 2022-10-13 10:41:51 -05:00
407ce2bc8f fix argument call to interactions 2022-10-13 10:26:22 -05:00
24d91f228b update interactions to allow building custom forms 2022-10-12 21:06:18 -05:00
53cbd11008 update shared library to fix data library issue 2022-10-12 12:14:43 -05:00
186db53bad update plugins to support command interception 2022-10-12 10:32:45 -05:00
40466f84c4 add command interceptor functionality 2022-10-11 16:18:56 -05:00
bdb5a1c5f8 Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-10-05 09:51:24 -05:00
5d9e2b3bf1 Game Interface ported to T5. (#254)
* Implement game interface for IW5 and T5
2022-10-05 09:49:00 -05:00
1cf99869f6 remove unneeded check for has permission 2022-09-24 10:22:05 -05:00
12da0f463b add client tag to default game interface data 2022-09-24 10:06:07 -05:00
e88071684d provide client tag in game interface meta 2022-09-21 13:04:15 -05:00
cd6097d133 default user permission for guest requests 2022-09-19 22:01:34 -05:00
d5cf4451a2 NoClip Fix - Removed NoClipOff - Toggle Hide (#263)
* Usage of Hide is now consistent with NoClip; toggleable
Removed obsolete !NoClipOff
2022-09-11 11:51:10 -05:00
1e1e8bbe7b fix issue with game interface meta/provide full example 2022-09-11 11:46:13 -05:00
dadd236069 upgrade nuget packages 2022-09-09 09:45:46 -05:00
2380f23dbe implement profile interaction registration through plugins (mute and vpn detection implementation) 2022-09-08 15:03:38 -05:00
3cffdfdd9d Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-09-07 09:16:58 -05:00
400c5d1f4d increase security on webfront cookie state/update events 2022-09-06 15:44:13 -05:00
ca35fbb19f iw4x integration - add delay before sending up persistent data 2022-08-31 16:17:02 -05:00
809cb0b7f4 account for trailing color code on long cod4x names 2022-08-27 21:25:42 -05:00
18f23fd07d Adding Mute for IW4x (#257)
* Adding Mute for IW4x
2022-08-26 12:09:33 -05:00
7526f86dab fix issues with game interface reconnecting after rcon connection lost 2022-08-26 12:07:43 -05:00
527ffbaced actual fix of setpassword from web console 2022-08-20 11:34:52 -05:00
6f086ac565 modularize the game integration files and better organize the anticheat folder structure 2022-08-20 10:57:03 -05:00
cf4dd6a868 fix issue with set password 2022-08-20 10:42:34 -05:00
3efafa24ff map the g_password dvar for T7 parser 2022-08-17 21:57:13 -05:00
fe919251fb add chat/chatteam event mapping for T7 2022-08-16 18:37:35 -05:00
a67f7f9351 don't display client banned on webfront if a linked ban has been revoked but they haven't reconnected yet 2022-07-25 11:54:55 -05:00
e99ca3c140 add more cases to "About" regex rule numbering scheme 2022-07-25 10:33:44 -05:00
ccedb01e8d improve help display and add supported games list 2022-07-25 10:21:08 -05:00
841bcf6156 tweak for T6 parser 2022-07-25 09:10:12 -05:00
b381af5fba fix dvar regex for T7 2022-07-24 13:29:40 -05:00
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
497c15a6a8 update stats to use new meta service 2022-03-23 13:54:42 -05:00
7be096e0b6 add vpn whitelist command 2022-03-23 13:34:04 -05:00
20858991e1 move live radar js into own file 2022-03-23 12:52:11 -05:00
85d44b0eb0 fix issue with multi line output freezing console 2022-03-23 12:09:40 -05:00
51ef67ae9c add BroadcastAsync 2022-03-23 11:43:20 -05:00
63b04be4c7 add tell async and update SharedLibraryCore version 2022-03-23 11:38:09 -05:00
36eb45bb2e mark old meta service as obsolete 2022-03-23 11:31:53 -05:00
04a4dcf153 implement metaservice v2 2022-03-23 08:43:57 -05:00
b46b1eb5e7 fix update on report to penalize flagged users 2022-03-23 08:22:23 -05:00
287635fa36 update integration gsc 2022-03-12 13:41:10 -06:00
f567a03fa7 implement team tracking via game interface (EFClient.Team and EFClient.TeamName) 2022-03-12 13:38:33 -06:00
1b6d8107ae Add T6 Weapon Name Parser Config (#236)
Add T6 Weapon Name Parser Config
2022-03-08 12:08:16 -06:00
1e8f06f3a3 Fix iw3 gamestring typo (#234)
RDP -> RPD
2022-03-08 12:08:04 -06:00
064879fead Add info api for #231 2022-03-08 12:06:46 -06:00
e32e97b9e6 fix issue with loading stats config #237 2022-03-08 11:24:59 -06:00
42313b7816 update action on report to use level enum string 2022-03-07 20:00:05 -06:00
9f4d06c265 refactor some game interface plugin approach 2022-03-07 19:59:34 -06:00
acf66da4ca tweak cod rcon connection and fix max health for hide integration command 2022-03-05 13:13:00 -06:00
59ca399045 more cod rcon tweaks 2022-03-03 08:54:17 -06:00
ef70496546 hopefully fix some issues with rcon socket 2022-03-02 18:21:08 -06:00
e6e56d8d14 add back helper methods without cancellation token for plugins 2022-03-02 08:29:15 -06:00
55b0caf900 tweak game interface values again 2022-03-02 08:28:41 -06:00
a4c3f9c2d1 update delete obsolete plugin migration 2022-03-01 12:47:35 -06:00
241aa0a5f6 tweak rcon timeout for script calls 2022-03-01 12:46:01 -06:00
ec0f59cdb1 add set spectator command for game interface 2022-03-01 12:45:39 -06:00
59d69bd22b add cancellation token for rcon connection to allow more granular control 2022-02-28 20:44:30 -06:00
e9c8ead829 simplify level update so we don't have to worry about linked account levels 2022-02-28 15:20:46 -06:00
58d48a211e make sure iw4madmin exits when selecting "no" to continue with failed server connections 2022-02-28 15:16:30 -06:00
edf8e03b04 don't refresh scoreboard on every page. though I fixed this already... 2022-02-27 21:35:16 -06:00
de2e804b84 improve meta filter menu on profile 2022-02-25 21:09:57 -06:00
b087d4c8de unescape utf characters when saving configs 2022-02-25 09:44:28 -06:00
bd6c0dd5be fix issue with tempban not displaying properly 2022-02-25 08:22:40 -06:00
4ace476242 mark permission changed as sensitive 2022-02-23 16:26:46 -06:00
bb7215dbb6 increment shared library references 2022-02-23 15:57:44 -06:00
88bd47f3ae add search ip shortcut on profile 2022-02-23 15:47:17 -06:00
39a1066c74 add permission level changed meta 2022-02-23 12:47:00 -06:00
18f3c59b9b allow search client exact with quotes 2022-02-23 09:32:59 -06:00
0d88b6293f add create/update times to penalty identifiers 2022-02-23 09:02:01 -06:00
a6b56ceded tweak for integration 2022-02-22 17:10:33 -06:00
78ef977268 simplify ban process with new system 2022-02-22 17:09:50 -06:00
d527a86911 improve mag command matching of maps and gametypes 2022-02-22 08:38:02 -06:00
2e531c4a50 validate game interface commands to ensure it's enabled before trying to execute 2022-02-18 10:15:11 -06:00
45059fcfd9 change mask command alias to not conflict with game interface hide 2022-02-18 10:04:48 -06:00
482cd9c339 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-02-15 20:23:30 -06:00
51667159a2 fix validation errors freezing initialization 2022-02-15 20:23:16 -06:00
ea18a286b2 improve error output when configuration is invalid 2022-02-15 20:16:21 -06:00
9a6d7c6a20 game interface improvements 2022-02-15 20:05:50 -06:00
adcb75319c Changed .NET 6 Direct Download... (#229)
Changed the .NET 6 Direct Download link to the hosting bundle as it includes both the .NET 6 runtime and the ASP .NET 6 runtime which are both needed.
2022-02-15 09:56:31 -06:00
037fac5786 game interface improvements 2022-02-13 21:38:40 -06:00
f4b892d8f4 improve network log support 2022-02-13 16:50:09 -06:00
3640d1df54 small updates for game interface 2022-02-12 21:54:21 -06:00
f3c6b10a35 add network game log reader ex: net.tcp://ip:port 2022-02-11 15:33:05 -06:00
4dec284b31 fix unnecessary output when not able to connect to all servers 2022-02-10 17:01:06 -06:00
c9cf7be341 add set client meta and inc/dec to framework 2022-02-10 16:50:45 -06:00
aa6ae0ab8d more integration tweaks 2022-02-09 14:45:28 -06:00
12dfd8c558 more integration tweaks
add configurable flood protect interval for rcon
2022-02-08 12:03:55 -06:00
07f675eadc fix issue with plugin registration 2022-02-07 22:02:50 -06:00
576d7015fa increase poll rate for reasonable response times 2022-02-07 18:47:16 -06:00
b1a1aae6c0 initial framework for gsc + iw4madmin integration
improvements to script plugin capabilities and error feedback
2022-02-07 18:43:36 -06:00
a0f4ceccfe small optimizations 2022-02-02 16:21:08 -06:00
b7a76cc4a2 only send heartbeat when fully initialized 2022-02-01 18:31:55 -06:00
261da918c7 Allow either parser version or parser name to be used in server config block 2022-02-01 18:27:03 -06:00
2ed5e00bcb more profile loading optimizations 2022-02-01 18:20:29 -06:00
6ca94f8da8 only default to IPv4 when parsing
update postgres target version to 12.9
2022-02-01 14:27:16 -06:00
3b532cf1f7 don't try to load scoreboard if not on scoreboard page 2022-02-01 09:09:29 -06:00
40966ed74d modify update script on linux to set executable bit on itself after update 2022-02-01 09:04:40 -06:00
45eacabc28 actual fix now? 2022-01-31 17:56:43 -06:00
0b02b7627a fix again 2022-01-31 17:23:56 -06:00
fc3a24ca17 fix typo on pipeline 2022-01-31 17:00:24 -06:00
209cb6cdd0 use proper folder in post publish script 2022-01-31 16:47:51 -06:00
cfd4296f5c update webfront ip lookup for ssl connection 2022-01-31 16:37:44 -06:00
b275fbaced create update script for managing updates programatically
./UpdateIW4MAdmin.sh or ./UpdateIW4MAdmin.ps1
Co-authored-by: xerxes-at <xerxes-at@users.noreply.github.com>
2022-01-31 11:06:44 -06:00
b2a3625288 update IP lookup api 2022-01-31 08:16:12 -06:00
0d3e2cb0bc fix issue with writing config files 2022-01-29 13:30:48 -06:00
505a2c4c2d fix refactor issue 2022-01-28 17:28:49 -06:00
8730a3fab8 fix issue with certain penalties not linking 2022-01-28 15:33:21 -06:00
3539101a40 webfront profile loading optimizations 2022-01-28 14:33:08 -06:00
7ccdee7d1b disable some warnings 2022-01-28 09:37:04 -06:00
f4b160b735 small startup performance optimization 2022-01-28 09:35:01 -06:00
73036dc1c7 properly provide culture to welcome plugin ordinalize 2022-01-27 21:19:05 -06:00
6cfcce23cc tech debt 2022-01-27 21:18:35 -06:00
8649b0efe9 fix issue with configuration on new install 2022-01-27 13:37:38 -06:00
f554536b95 s This is a combination of 7 commits.
This is the 1st commit message:
2022-01-27 11:25:42 -06:00
11efc039b5 update for .net core SDK Azure 2022-01-27 09:35:16 -06:00
916ea4163b add additional fields to server api 2022-01-26 15:26:26 -06:00
0bed1c728a update .net version required in readme 2022-01-26 15:26:25 -06:00
7171b3753e Address some .NET 6 oddities and allow webfront startup without servers being monitored 2022-01-26 15:26:25 -06:00
a602e8caed Initial .net 6 upgrades 2022-01-26 15:26:25 -06:00
e4cb3abb20 order chat context messages from oldest to newest 2022-01-26 15:26:25 -06:00
686b297d32 hopeful topstats fixes 2022-01-26 15:20:10 -06:00
fb11bf54a6 scoreboard tweak 2022-01-26 15:20:10 -06:00
11d2b0da90 display "--" for no zscore 2022-01-26 15:20:10 -06:00
8bd0337168 scoreboard sort tweak 2022-01-26 15:20:10 -06:00
74b565ebae increase zscore precision for scoreboard.. last commit I promise 2022-01-26 15:20:10 -06:00
2b467d6ef9 fix missing null check in scoreboard. oops 2022-01-26 15:20:10 -06:00
e90355307d include cs go "estimated" score on scoreboard 2022-01-26 15:20:10 -06:00
d3962989b5 add sorting and zscore to scoreboard 2022-01-26 15:20:10 -06:00
16831aaccb remove incorrect project reference 2022-01-26 15:20:10 -06:00
032753236b fix misc webfront errors on first run after configuration 2022-01-26 15:20:10 -06:00
7fcb2202bd add server scoreboard functionality 2022-01-26 15:20:10 -06:00
7910fc73a3 increment shared library version 2022-01-26 15:20:10 -06:00
a8d581eab7 Update shared library to reference data library instead of separate nuget package 2022-01-26 15:20:10 -06:00
bd27977b1e improve connection resets in CSGO 2022-01-26 15:20:10 -06:00
092ca5f9bd Update plutonium t4 MP parser 2022-01-26 15:20:10 -06:00
3f0b1b892a Add Plutonium T4 Co-Op/Zombies support 2022-01-26 15:20:10 -06:00
c713fdacb0 update packages for previous release (re-release of previous) 2022-01-26 15:20:10 -06:00
f5854f8d03 hopefully fix issue with linked banned players 2022-01-26 15:20:10 -06:00
67be4f8e7f reduce some potential errors 2022-01-26 15:20:10 -06:00
9baad44ab4 update max name length to 34 for base kill/damage parser 2022-01-26 15:20:10 -06:00
76f5933074 fix color code issue 2022-01-26 15:20:10 -06:00
4cce336fb9 update custom callbacks to properly exit thread on disconnect 2022-01-26 15:20:10 -06:00
5d12ff471b work around for iw5/t6 not being able to parse multiple commands over rcon for mag command 2022-01-26 15:20:10 -06:00
5d7ac7498f update to show full gametype name on webfront 2022-01-26 15:20:10 -06:00
15cb114c15 implement map and gametype command 2022-01-26 15:20:10 -06:00
e739c91b52 add color code mapping for CSGO 2022-01-26 15:20:10 -06:00
17c9944eef fix concurrency issue with accent color setup 2022-01-26 15:20:10 -06:00
4a89744ee9 abstract engine color codes to use (Color::<Color>) format to make codes more.
see pt6 parser and configs for example usages
2022-01-26 15:20:10 -06:00
66010a2fa2 fix issue with caching implementation 2022-01-26 15:20:10 -06:00
ce3119425f try renable FTP publish 2022-01-26 15:20:10 -06:00
307ff3ddeb update help command to use per game commands 2022-01-26 15:20:10 -06:00
7f2fa390c7 fix plugin error formatting 2022-01-26 15:20:10 -06:00
a88b30562c update caching to use automatic timer instead of request based to prevent task cancellation 2022-01-26 15:20:10 -06:00
08bcd23cbc add default port and rcon password hint during setup 2022-01-26 15:20:10 -06:00
072571d341 add console log sink for critical errors 2022-01-26 15:20:10 -06:00
35e42516f1 update plugin error message format 2022-01-26 15:20:10 -06:00
2210ccea68 update webfront ip lookup to bypass api key restriction 2022-01-26 15:20:10 -06:00
08b93fcc10 Add Pluto IW5 Maps from r2385 (#220) 2022-01-26 15:20:10 -06:00
ab05b45016 fix issue with assigning correct server when processing command 2022-01-26 15:20:10 -06:00
825dd6f382 update country flag api 2022-01-26 15:20:10 -06:00
f99fdac4b0 remove javascript error log trying to load hljs from non config pages 2022-01-26 15:20:10 -06:00
f7897763e3 temporarily disable ftp release integration to bypass unknown Error: connect ETIMEDOUT *:21 (control socket) 2022-01-26 15:20:10 -06:00
5b95cdaca8 update welcome plugin to bypass api lookup limitation 2022-01-26 15:20:10 -06:00
c4e0c4c36a cleanup and enhance penalty handling 2022-01-26 15:20:10 -06:00
31d0dfc7d3 reduce timeout when master api is down 2022-01-26 15:20:10 -06:00
8f52714fb7 fix issue with detecting bans on accounts with new ips when implicit linking is disabled 2022-01-26 15:20:10 -06:00
e4153e0c2f post webfront url to master 2022-01-26 15:20:10 -06:00
8d0c48614f Merge pull request #219 from RaidMax/release/pre
Merge pre release into master
2021-10-19 20:52:49 -05:00
761d156209 Merge branch 'master' into release/pre 2021-10-19 20:45:05 -05:00
77f04058de merge default settings up 2021-10-19 20:40:40 -05:00
1317102d00 add script injection to the config to import custom webfront scripts (ie google tracking) 2021-10-19 20:17:10 -05:00
a2c7d92162 fix issue on about page with duplicate server names or inactive servers 2021-10-19 20:02:31 -05:00
b2afc410f2 improve about page layout 2021-10-16 13:30:26 -05:00
5b3420b97a default about page to enabled 2021-10-10 10:57:27 -05:00
74bb3da459 add option to toggle about page/make some checks on displayed rules 2021-10-10 10:44:18 -05:00
3916278422 Add about/community info guidelines/social page 2021-10-09 21:11:47 -05:00
a01543c89b deactivate penalties while unlinking an account if implicit account linking is disabled 2021-09-30 10:28:04 -05:00
694431d789 fix profile display with implicit linked accounts enabled 2021-09-18 22:31:56 -05:00
d5f978858d set sv_sayname on connection restore 2021-09-18 18:28:37 -05:00
e80753a4d3 make connection attempts for CoD configurable as "ServerConnectionAttempts" 2021-09-18 18:25:02 -05:00
d4fb75d07c add check to determine whether to include color codes when checking name length 2021-09-18 18:10:47 -05:00
e97119211f fix source issue on home page 2021-09-17 11:23:57 -05:00
87985b3e68 cap client name for new flow 2021-09-17 11:19:17 -05:00
33c63f01db add raw file editing to configuration page in webfront 2021-09-16 16:27:40 -05:00
68c1151191 add tooltip timestamp to max concurrent players 2021-09-14 18:12:20 -05:00
54e39fabb1 fix client history issue with empty database 2021-09-10 11:27:46 -05:00
a4f0726b32 Merge remote-tracking branch 'origin/release/pre' into release/pre 2021-09-06 11:37:30 -05:00
05e228633d fix searching name resulting in incorrect results 2021-09-06 11:37:15 -05:00
e267bd95da Update IW6x parser to automatically find the log file. (#216)
* Update ParserIW6x.js
2021-09-05 10:45:28 -05:00
c7fab5d36c removed commented code and show current alias for ip search 2021-09-05 10:43:48 -05:00
1f8b7cde3f test linking fix 2021-09-04 12:33:25 -05:00
c5f9a68102 implement client server connection tracking persistence 2021-08-31 18:21:40 -05:00
eff8a29a39 version css for webfront 2021-08-31 18:07:07 -05:00
0191c8b7a7 bugfix for edge case of linking alias to new account 2021-08-31 09:53:01 -05:00
fa6524c3b1 fix issue with display server with no saved player history 2021-08-31 08:44:15 -05:00
5b11196b29 bundle js by version so webfront updates don't need a cache refresh 2021-08-30 20:30:06 -05:00
3b7a22edef tweak player history hover format 2021-08-29 20:47:25 -05:00
deff4f2947 persist client count history data across reboots and allow for configurable timespan 2021-08-29 13:10:10 -05:00
02e5e78f67 update iw5 parser to work around filesytem dvar limitation 2021-08-28 17:56:41 -05:00
162006da29 use new cache signature 2021-08-27 21:05:30 -05:00
27e9ecfd9d support homepath in pluto t6 2021-08-27 20:47:06 -05:00
da301bef40 Exclude accidental dotnet bundle command comment 2021-08-26 17:37:01 -05:00
a815bcbff5 Add max concurrent players over 24 hours badge to home 2021-08-26 17:35:05 -05:00
19a49504b8 display "since last connection" as per server on top stats instead of last connection to any servers 2021-08-25 17:47:57 -05:00
3bb87dffb0 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-08-25 11:07:32 -05:00
02942e5c03 Add support for IW5 (#213) 2021-08-25 11:06:52 -05:00
8c5ff440db Updated T6 AC GSC (#214)
* PlutoT6 AC GSC Updated

PlutoT6's GSC modding capabilities changed, this allows us to bring the script on parity with the IW4x one. The following things changed:
*  Script no longer replaces stock GSC since custom GSC files are now supported.
* The Script now captures the last time the client used his attack button; this is used to detect trigger bots.
* Cleaned up the code a bit

* Create README.MD

Basic installation guide.
2021-08-25 11:06:46 -05:00
a0b7781e66 properly unban accounts associated with IP with toggle 2021-08-25 11:02:37 -05:00
596272a3de tweak linking behavior 2021-08-21 10:40:03 -05:00
b83ea57579 fix another thing 2021-08-16 18:28:00 -05:00
75f68b6385 remove other changes 2021-08-16 17:13:17 -05:00
d5e4d083c5 renable dotnet bundle cuz that was the real issue. 2021-08-16 17:02:47 -05:00
602ec66afe more pipeline test plz work 2021-08-16 16:53:58 -05:00
435b079b94 testing again for CLI Version 2021-08-16 16:46:18 -05:00
a4eec5981f specify explicit .net cli sdk version for pipeline 2021-08-16 13:50:22 -05:00
0b6e261dbb fix more issues with implicit link toggle 2021-08-16 13:20:54 -05:00
7e1221f467 fix small issue with new toggle 2021-08-14 20:43:20 -05:00
a6b0911af9 make implicit account linking a feature toggle 2021-08-14 17:55:28 -05:00
fa66381193 small fixes 2021-08-14 11:30:15 -05:00
67c2406325 fix issues with last release 2021-07-12 14:57:44 -05:00
e2ea5c6ce0 support hostnames for server config 2021-07-11 17:26:30 -05:00
5ef00d6dae tweak headshot detection for CSGO 2021-07-11 09:58:02 -05:00
5921098dce detect headshots for CSGO on advanced stats
track say_team events for CSGO
2021-07-10 21:37:51 -05:00
31ee71260a use default settings for maps and quick messages config (remove from IW4MAdminSettings) 2021-07-09 16:50:33 -05:00
ed8067a4a2 add offline messaging feature 2021-07-08 21:12:09 -05:00
e2116712e7 pass x-forwarded-for to properly log proxied login/logout 2021-07-05 16:08:13 -05:00
8b06da5783 use different api for country code/flag that support https 2021-07-02 10:04:56 -05:00
33a427bb8a add country flag and name to profile 2021-07-01 21:58:09 -05:00
c9d7a957dc add reset anticheat metric (!rsa) for issue #177 2021-07-01 13:12:19 -05:00
9c6ff6f353 use right game for estimated score 2021-07-01 13:06:31 -05:00
7444cb6472 actually fix steam id parsing 2021-07-01 10:14:58 -05:00
c7e5c9c8dd parse steam id properly for source games 2021-07-01 09:10:56 -05:00
0256fc35d2 add login/logout events to change tracker
default guest profile to minimum permissions
2021-06-30 21:13:25 -05:00
0019ed8dde fix run as command config not being honored properly 2021-06-30 18:10:45 -05:00
56aec53e72 fix bad key lookup in manager 2021-06-30 14:01:41 -05:00
1b773f21c6 fix alignment for long server names 2021-06-30 10:44:43 -05:00
bccbcce3c1 add lobby rating to home
add gametype (WIP) to home
misc UI tweaks
2021-06-30 09:57:07 -05:00
fc0bed2405 show "out of" ranked players for stats command 2021-06-29 17:14:25 -05:00
16cfb33109 improvements and consistencies to the top stats, most played and top players commands 2021-06-29 15:35:56 -05:00
42979dc5ae Use string for AC snapshot weapon and hit location
Add webfront logging
2021-06-29 15:02:01 -05:00
95cbc85144 fix issue with selecting wrong parser during setup
add minimum name length option
fix issue with stats spm
2021-06-27 20:31:39 -05:00
9cbca390fe Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-06-16 08:55:56 -05:00
38c0c81451 Added CSGO maps (#210)
Added all current default CSGO maps (Competitive, Wingman, Casual, War Games, Retakes, Danger Zone)
2021-06-16 08:54:49 -05:00
af4630ecb9 Additional CSGO compatibility improvements 2021-06-16 08:53:50 -05:00
dbceb23823 fix issue with custom event registration 2021-06-16 08:51:22 -05:00
e628ac0e9e improve CS:GO compatibility 2021-06-11 11:52:30 -05:00
3a1e8359c2 add one log indicator for games (Pluto IW5) that don't log to mods folder even when fs_game is specified. 2021-06-07 16:58:36 -05:00
c397fd5479 update pluto iw5 parser for new version
fix issue with finding players with color codes in name
2021-06-06 13:40:58 -05:00
16e1bbb1b5 fix bug with additional group mapping key 2021-06-03 13:21:34 -05:00
eff1fe237d Fix null pointer exception (#207) 2021-06-03 10:52:27 -05:00
b09ce46ff9 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-06-03 10:51:19 -05:00
be08d49f0a add initial CS:GO support 2021-06-03 10:51:03 -05:00
b9fb274db6 Update ParserPT6.js (#206) 2021-05-15 09:22:34 -05:00
9488f754d4 Fix stupid idiot things 2021-05-15 09:20:49 -05:00
1595c1fa99 Initial implementation of configuration support for script plugins 2021-05-14 21:52:55 -05:00
4d21680d59 small issue fix with api and more checks for welcome tags 2021-05-04 19:01:09 -05:00
127af98b00 fix issue with help and dynamically loaded plugins with commands 2021-04-30 12:37:55 -05:00
21a9eb8716 Update DefaultSettings.json (T4, IW5, S1x) (#202)
* Update DefaultSettings.json
2021-04-30 12:35:38 -05:00
f1593e2f99 fix issue with chat message search 2021-04-18 09:17:01 -05:00
74dbc3572f Added WaW bot guid (#200)
may be PlutoniumT4 only.
2021-04-16 13:48:52 -05:00
e6d149736a Added T4 weapon names. (#198) 2021-04-16 13:47:58 -05:00
a034394610 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-04-16 13:38:34 -05:00
34e7d69110 Add RCon support for S1x 2021-04-16 13:35:51 -05:00
4b686e5fdd Update Plutonium T4 Parser [v0.2]
Static version string
2021-04-08 09:36:32 -05:00
0428453426 Update Pluto T4 Parser
Uses new static version string.
2021-04-08 09:36:32 -05:00
e80e5d6a70 remove test code 2021-04-07 09:53:32 -05:00
22cf3081e1 update parser for Plutonium T4 2021-04-07 09:50:41 -05:00
76a18d9797 add parser support for Plutonium T4 2021-04-07 09:33:49 -05:00
fc13363c9c add user agent header for vpn detection issue #195 2021-04-07 08:47:42 -05:00
f916c51bc0 fix issue with iw5 weapon prefix not being removed properly 2021-04-01 13:12:47 -05:00
21087d6c25 remove whitespace on alias display and client name search 2021-03-31 11:20:32 -05:00
c84e374274 fix issue with client api for issue #191 2021-03-27 19:01:27 -05:00
e777a68105 properly pass game name to game string config finder.
add weapon prefix to weapon name parser for (iw5).
add some iw3 game strings
2021-03-23 21:42:26 -05:00
1f9c80e23b strip colors from header penalty on profile 2021-03-23 21:42:26 -05:00
33371a6d28 Added iw6 aliases (#184) 2021-03-23 21:42:26 -05:00
8530444ffa Added T7 aliases (#186)
1. T7 aliases by @mikzyy
2. Highrise for IW5 which is still in beta
2021-03-23 13:14:11 -05:00
Edo
d164ef2eab Removed tempbanclient (#187)
Removed tempbanclient because Tekno has "weird" internal DB that manages temp bans it it would interfere with iw4m
2021-03-23 10:36:33 -05:00
e2ed57f674 prevent autoflag from running player has been manually unflagged 2021-03-23 10:34:44 -05:00
824b1c0990 prevent loading of privileged clients page for issue #188 2021-03-23 10:28:17 -05:00
a8b331a5e5 prevent missing config from causing stats error
small advanced stats fixes
2021-03-23 10:16:27 -05:00
802ec8cea5 Added iw6 aliases (#184) 2021-03-23 08:14:07 -05:00
2313c4357b add removal of obsolete plugins 2021-03-22 11:46:32 -05:00
c5375b661b huge commit for advanced stats feature.
broke data out into its own library.
may be breaking changes with existing plugins
2021-03-22 11:09:25 -05:00
db2e1deb2f modify rule shortcut to just have 1 list 2021-02-27 09:40:25 -06:00
191a68e7dd revert unintended commit file 2021-01-24 13:30:22 -06:00
c4f19e94ef implement custom tag (descriptor) feature
allow override of level names through configuration
few small fixes/improvements
2021-01-24 11:47:19 -06:00
2512b9f251 Added iw6 aliases (#184) 2021-01-20 12:43:44 -06:00
c419d80b57 preemptive checks 2021-01-17 22:12:18 -06:00
23a33ba489 implement more robust command api and login
improve web console command response reliability and consistency
2021-01-17 21:58:18 -06:00
dd3ebf6b34 increase buffer size for rcon connection 2021-01-17 20:04:32 -06:00
28373b9325 implement admin "privacy" for issue #185 2021-01-09 12:37:20 -06:00
843c01061d update 'uptime' output
use translations for certain webfront page meta that was neglected
update plutonium parsers to not use new line in notices as it is not supported
2021-01-08 19:21:23 -06:00
5cb2d05f33 add preset rules, configurable time spans, and separate rule shortcut for issue #180 2020-12-31 18:48:58 -06:00
5a288dafc1 update shared library core version and plugins 2020-12-20 19:23:14 -06:00
4afc478076 fix issue with view stats and reset stats failing
fix issue with set level returning wrong error message if setting a client to the same level they're currently at
update CoD4x parser version
update nuget packages
2020-12-16 13:11:30 -06:00
928cbef845 resolve bot guid issue with T5
remove unneeded check for CNCT state
2020-12-14 21:10:50 -06:00
02b910234a add official T4/WaW support for issue #178
CoD4x parser tweak to parse full guid as decimal
2020-12-13 20:33:37 -06:00
f03626c3ae Another tweak for CoD4x rcon parsing. 2020-12-12 21:43:27 -06:00
6648b75255 update CoD4x parser
tweak handling segmented status response
actually support more than 18 clients LOL
2020-12-02 14:29:49 -06:00
bd3f0caf60 fix memory leak issue related to AddDbContext not working as expected 2020-11-29 16:01:52 -06:00
b2d282d412 include ; for timeout string 2020-11-27 22:08:13 -06:00
36a02b3d7b update for database provider specific migrations
fix issues with live radar
2020-11-27 21:52:52 -06:00
8ef2959f63 make notice line separator configurable for different parsers
(updated tekno's as it doesn't support \n)
2020-11-19 20:48:25 -06:00
d58b24b5b2 add shortcut for rules in penalty reasons for issue #159 2020-11-18 18:48:51 -06:00
09f37d7941 clean up some logic related to tracking stats on player join 2020-11-18 16:28:14 -06:00
103d2726c2 persist say command messages with webfront denotation to chat log
per issue #159
2020-11-18 09:08:24 -06:00
941d9cea73 more consistent/enhanced game penalty messages per issue #171 2020-11-17 18:24:54 -06:00
a574fb0d4b update index for ratings/prune old entries
small stat tweaks to add players on first kill/damage event
(instead of on connect which causes issues with slow writes)
2020-11-14 18:24:51 -06:00
664eb32587 fix small logging issue with loading plugins
add minigun turret to list of ignored ac weapons
2020-11-14 10:53:01 -06:00
6619ce714a modify iw6x parser to default game log vars temporarily, small amount of code cleanup to git rid of warnings 2020-11-12 20:39:56 -06:00
e997b94b3b update unit tests 2020-11-12 19:46:17 -06:00
5d9c8f5369 fix introduced issue with map/map_rotate commands 2020-11-11 18:53:23 -06:00
570a228c92 refactor logging in pretty big overhaul 2020-11-11 17:35:55 -06:00
fd7bd7e0da partial support of IW6x until the game log is implemented 2020-11-07 10:40:58 -06:00
e76976799b fix issue with partial matches for map load command 2020-11-03 20:04:11 -06:00
84189cf136 fix issue with T5 status response parsing 2020-10-31 09:18:37 -05:00
98ee997bf3 update pipeline versioning 2020-10-25 10:03:15 -05:00
3f7372e780 add pre release pipeline to master 2020-10-24 21:45:30 -05:00
08676f1d1e implement remote assembly loading 2020-10-24 15:02:38 -05:00
2bbafbd8f0 fix issue with delay on map command 2020-10-17 10:55:49 -05:00
40cb2a9df6 add say all (broadcast) command 2020-10-17 10:55:42 -05:00
59f1699228 fix issue with button detection 2020-10-17 10:55:29 -05:00
1484d63b97 hide flag status for non logged in users
remove erroneous anticheat detection reason on kick
2020-10-17 10:55:19 -05:00
04217e96ee fix anticheat detection type logic 2020-10-17 10:54:54 -05:00
c41fc27a1a fix introduced bug :) 2020-09-30 21:00:40 -05:00
1f1f4de67a anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-09-30 17:15:47 -05:00
7f11921757 enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-09-28 20:32:53 -05:00
70cae976a0 implement service resolver for script plugins 2020-09-26 18:13:56 -05:00
2ab0cfa9be implement pm admins command for issue #170 2020-09-26 17:17:21 -05:00
7e3c74e63c add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-21 15:32:49 -05:00
a4a65a486a update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar
2020-09-21 15:30:42 -05:00
ac06b41a0b update shared library version 2020-08-31 12:31:40 -05:00
cce6482541 allow tracking of "zombie" clients to support stat tracking in zm 2020-08-31 12:13:20 -05:00
bc7dc3a71a Add XuidString and GuidString to EFClient to allow easier interfacing with mods 2020-08-31 12:03:06 -05:00
2be719d8f9 add website override mapping to tekno parser (_website -> sv_clanWebsite) 2020-08-31 11:58:56 -05:00
8a8dec8bbd remove hard coded paths to make it easier for building in debug mode
auto copy script plugins/localization for local builds
2020-08-26 09:54:56 -05:00
2b3e21d4ba fix most played formatting issue
prevent reverse proxy to 127.0.0.1 from counting as IW4MAdmin client
copy humanizer support lib to output dir
2020-08-21 18:12:00 -05:00
4590d94d7d update bundle minifier package to use .net core one 2020-08-20 13:10:43 -05:00
5842073f91 include "all" meta button on profile
include full humanizer package to library bug in russian translations
2020-08-20 11:08:21 -05:00
c783a04a52 hide chat for password protected servers for issue #162 2020-08-20 10:38:11 -05:00
4735864113 remove some left over warnings from deprecated packages 2020-08-19 14:50:49 -05:00
d70d8fd0ae merge 2020-08-18 20:15:46 -05:00
0dc4e12d61 another attempt to fix display of long client names/temporary t6 getinfo workaround 2020-08-18 20:11:41 -05:00
778e339a61 QOL updates for profile meta
implement filterable meta for issue #158
update translations and use humanizer lib with datetime/timespan for issue #80
2020-08-18 16:35:21 -05:00
1ef2ba5344 fix misaligned kick button with long names on webfront 2020-08-18 16:35:21 -05:00
126f2fcc47 Merge branch '2.4-pr' of https://github.com/RaidMax/IW4M-Admin into 2.4-pr 2020-08-18 16:33:45 -05:00
d5789dac81 Create FUNDING.yml (#161) 2020-08-12 20:48:07 -05:00
25e2438e7f fix misaligned kick button with long names on webfront 2020-08-12 13:46:14 -05:00
19107f9e85 Update README.md 2020-08-11 20:48:13 -05:00
0e44fa10f7 Consolidate README (#156)
* Consolidate README.
We use the wiki now for most information, this just reduces "duplicate" data.
2020-08-06 13:20:35 -05:00
ebb54ebfd7 Add ManualWebFrontURL to readme. (#150)
* Update README.md

* Update project links
2020-08-06 08:49:20 -05:00
03a27d113e Merge pull request #105 from xerxes-at/2.4-pr
Added support for the AC to PlutoT6
2020-08-06 08:48:53 -05:00
22f9e581ed fix dependency injection of comands in webfront preventing ui actions from working 2020-08-06 08:48:14 -05:00
b59504a882 grab gametype from status for T7 2020-08-05 09:43:31 -05:00
ed2b01f229 update action controller to dynamically generate command names in case of overridden names (issue #152) 2020-08-04 17:26:16 -05:00
f040dd5159 fix mislabled dragunov name in live radar 2020-08-01 18:14:29 -05:00
6c00cceb7a update stats plugin to properly use the new configurable broadcast prefix. 2020-08-01 09:58:23 -05:00
04a95aa58a add configurable command and broadcast command prefix for issue #149 2020-07-31 20:40:03 -05:00
6155493181 prevent action on report from activating on privileged clients 2020-07-27 16:22:07 -05:00
297e2c283f Merge branch '2.4-pr' of https://github.com/RaidMax/IW4M-Admin into 2.4-pr 2020-07-27 11:26:37 -05:00
d8626bf70c Merge pull request #148 from RaidMax/feature/issue-144-report-action
implement action on report plugin for issue #144
2020-07-25 21:17:12 -05:00
c288184171 implement action on report plugin for issue #144 2020-07-25 21:15:46 -05:00
021c0244b4 remove old test project 2020-07-15 10:11:37 -05:00
214d15384d remove discord deprecated discord webhook file, remove game log file as it's being moved to new repo 2020-07-15 10:09:58 -05:00
36949bbf33 tweak color of kick icon 2020-07-14 15:48:38 -05:00
88b1f08149 add kick client functionality to webfront home for issue #142 2020-07-14 14:13:40 -05:00
4c583e1c53 remove master project 2020-06-30 16:42:30 -05:00
6e95a7b015 support custom master url
refactor api instatation to allow custom master url in config
2020-06-30 16:39:32 -05:00
a013a1faf0 prevent ability to kick users of same rank 2020-06-17 15:20:07 -05:00
bb4e51d9c8 adjustments for T6 and tekno (implement mapped dvars and default values) 2020-06-16 17:16:12 -05:00
ba77e0149c disable standard console in if it has been redirected 2020-06-03 19:45:06 -05:00
b8d5495055 include client name in stats info result 2020-05-30 14:14:42 -05:00
fa79f4af73 fix issue with registering multiple script commands in command configuration 2020-05-30 14:06:04 -05:00
cad2952c46 [issue #140]
fix bug with friendly fire being disabled with custom callbacks on IW4x
2020-05-30 13:39:09 -05:00
43ac1218cc fix shared library linking issue 2020-05-25 14:09:41 -05:00
aef1ac6aae Merge pull request #141 from RaidMax/feature/issue-139-stats-api
[issue #139] client lookup and stats api
2020-05-25 13:06:44 -05:00
30f2f7bf09 [issue #139] client lookup and stats api 2020-05-25 13:04:44 -05:00
4457ee5461 Merge pull request #138 from RaidMax/feature/issue-137-custom-hostname
[issue 137] custom display hostnames for webfront
2020-05-23 13:26:07 -05:00
e91c60a753 [issue 137] custom display hostnames for webfront 2020-05-23 13:25:09 -05:00
1241ac459e re-enable claims permission add/remove 2020-05-22 21:38:38 -05:00
4afd1f3cdc Merge pull request #136 from RaidMax/feature/issue-135-enhanced-search
[issue 135] enhanced search
2020-05-22 20:35:42 -05:00
5042ea6c91 [issue 135] enhanced search
implement enhanced search for chat messages
2020-05-22 20:29:41 -05:00
bef5ffbd35 update IW5 parser 2020-05-19 11:01:08 -05:00
19f5f557bd update readme / upgrade game log server packages to work with latest python release 2020-05-18 21:03:40 -05:00
6aa6af526a fix issue with counting plugin tasks causing them to be executed. why ms? 2020-05-17 17:01:13 -05:00
0cabf6f8a3 only fix double forward slash characters (instead of single) when sending messages
retry kicks on banned players if they're banned from webfront, but don't actually get kicked because the game doesn't process the command (looking at you T6)
allow capturing chat messages for names spoofed to an empty string
make sure mostkills uses days not month for cutoff
2020-05-16 20:55:18 -05:00
d3d1f31ee0 bugfixes/enhancements
prevent users from trying to set the console's level to owner
fix issue with setting multiple owners
update/improve unit tests
2020-05-16 11:54:01 -05:00
420e0d5ab5 Merge pull request #133 from RaidMax/feature/issue-132-script-command-registration
implement script plugin command registration - issue #132
2020-05-11 16:21:33 -05:00
2bd895e99d implement script plugin command registration - issue #132 2020-05-11 16:20:25 -05:00
44cacc1741 Merge pull request #130 from RaidMax/feature/issue-126-most-kills-command
[issue #129]
2020-05-05 18:50:35 -05:00
aff19b9577 [issue #129]
Add most kills command/macro
sneaky fix for tekno parser
2020-05-05 18:49:30 -05:00
267e0b8cbe [tweaks and fixes]
reenable tekno support
address vagrant thread issue
refactor game log reader creation to follow better practices
fix bot issues/address how guids are generated for bots/none provided
2020-05-04 16:50:02 -05:00
b49592d666 fix latent issue with password login due to not retreiving password/salt
set semaphore count properly for event execution throttling
2020-04-29 17:05:36 -05:00
c82139b88c small tweak to hopefully prevent too many events executing simultaneously 2020-04-29 16:27:24 -05:00
33712f3d7d update shared library nuget version 2020-04-28 18:19:46 -05:00
9dfdf5a82b remove debug output for ef 2020-04-28 17:54:06 -05:00
f5b0167f81 Merge pull request #128 from RaidMax/feature/issue-126-create-run-as-command
Feature/issue 126 create run as command
2020-04-28 16:49:40 -05:00
7715113b56 implement audit log view in webfront 2020-04-28 16:48:06 -05:00
58bfd189d0 [issue #126]
implement basic run-as functionality
2020-04-26 21:12:49 -05:00
3645cf53ff update default profanity filters to have something a little more usable 2020-04-26 15:57:51 -05:00
8a98ed7c50 small tweak for preconnect events 2020-04-26 12:32:41 -05:00
5529858edd [misc bug fixes]
properly hide broadcast failure messages if ignore connection lost is turned on
fix concurent issue for update stats history that happened with new event processing
make get/set additional property thread safe
add ellipse to truncated chat messages on home
2020-04-25 19:01:26 -05:00
ff011be8a6 unmeme the build 2020-04-22 21:08:25 -05:00
b41c4c6245 include some of the changes meant for previous build 2020-04-22 20:51:04 -05:00
92a26600af actually fix the session score concurrency issue
fix rare bug with shared guid kicker plugin
allow hiding of the connection lost notification
2020-04-22 18:46:41 -05:00
9e74dac5ed fix stat issue with concurrent threads
fix potential lost penalty if server does not response to kick request
make sure that broadcast only shows one custom say name
add unit tests
2020-04-21 17:34:00 -05:00
3ae2e42718 properly implement sv_sayName for custom say name
prevent trying to register live radar page for every server (oops)
optimize event processing to prevent slow plugins from affecting command processing
enable database connection resilency
trim extra characters from T7 reassembled response
2020-04-20 10:45:58 -05:00
0b643b2099 unmeme a dvar check 2020-04-18 17:48:49 -05:00
ee087f1c85 fix T7 extra null bytes in status response
fix regression bug with info response on T6
2020-04-18 10:46:55 -05:00
8c29027b3f partial T7 (BO3) support. includes rcon communication improvements and a small fix for displaying live radar tab 2020-04-17 15:05:16 -05:00
5bc1ad5926 fix regression issue with wine drive name mangling 2020-04-14 15:46:14 -05:00
c376266090 Merge pull request #123 from RaidMax/feature/issue-77-allow-server-type-categorization
add server categorization feature (issue #77)
2020-04-13 20:27:14 -05:00
8539223a15 add server categorization feature (issue #77) 2020-04-13 20:26:13 -05:00
b188e36786 update for new pluto iw5 rcon response 2020-04-13 19:43:24 -05:00
fca47cbce0 fix regression issue with log paths oops 2020-04-13 18:15:46 -05:00
be8041b868 refactor and test log path generation to support pluto IW5 better 2020-04-13 16:16:31 -05:00
b63d2995ed allow auto log filepath generation for pluto iw5 2020-04-12 20:48:03 -05:00
8fb2394130 support for Plutonium IW5 and only show live radar tab if monitoring at least one IW4 serves 2020-04-11 18:05:18 -05:00
36af673fc7 add ability to register custom event generators for event parsers / truncate long client names fix 2020-04-04 12:40:23 -05:00
9fdf4bad9c fix for runaway regular expression on linux
explicitly set string dvars in quotes to allow setting empty dvars
allow piping in input from command line (#114)
update the distribution for top stats elo
prevent game log file rotation from stopping event parsing
2020-04-01 14:11:56 -05:00
02a784ad09 allow prompt string to have an empty/default value
upgrade some project dependencies
don't try to run events on parsers
update top players rank distribution
2020-02-17 10:05:31 -06:00
2e5ffe91fc fix a small bug with new line truncation missing 2020-02-12 15:11:43 -06:00
68490bde57 Merge pull request #113 from RaidMax/enhancement/issue-112-toggle-automated-penalties-webfront
allow toggle of automated penalties display on the webfront
2020-02-12 13:16:11 -06:00
f430dab3a7 allow toggle of automated penalties display on the webfront
issue #112
fix small issue with script plugin loading
2020-02-12 13:13:59 -06:00
c3c21a7749 refactor a good bit of stuff for better dependency injection
fix regular expression for T6 log parsing
2020-02-11 16:44:06 -06:00
ec053eb854 Merge pull request #111 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
re-kick working as expected now
2020-02-07 11:16:27 -06:00
33494197e3 re-kick working as expected now 2020-02-07 11:15:21 -06:00
239ca30fd1 Merge pull request #110 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
fix disconnect event being cancelled
2020-02-06 21:06:38 -06:00
1dd88cdacb fix disconnect event being cancelled 2020-02-06 21:05:50 -06:00
f0f9a6beda Merge pull request #109 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
Use game time from log to ignore potential false disconnect lines - F…
2020-02-06 18:36:16 -06:00
fe380ca331 Use game time from log to ignore potential false disconnect lines - Fix for latent linking issues with multiple ips - Anticheat fix for T6 - retry kick on update if they're not allowed to connect 2020-02-06 18:35:30 -06:00
15e2170100 just a small fix that I forgot to include in the last build. 2020-02-03 08:21:42 -06:00
2872d02c37 fix plugin error spam with multi-servers 2020-02-02 16:21:34 -06:00
60ff33834e make sure we have an empty command config during initial startup, oops. 2020-02-01 13:28:05 -06:00
06cdaef8a4 allow Kekno to run with sv_running not returning anything :upside_down:
make sure script plugins output correct errors instead of being swallowed
prevent webfront error when webfront tab is left open on a server no longer being modified
2020-02-01 12:27:14 -06:00
c6d6bebeab Merge pull request #107 from RaidMax/feature/issue-104-allow-per-command-permission-config
Feature/issue 104 allow per command permission config
2020-01-31 20:23:37 -06:00
31c259f966 merge dev changes 2020-01-31 20:22:59 -06:00
318a23ae5b Finish implementation of configuable command permissions 2020-01-31 20:15:07 -06:00
11ae91281f start work to allow customizing command properties via configuration 2020-01-26 18:06:50 -06:00
1fd31beb05 fix another meme 2020-01-26 15:40:00 -06:00
116c909c2d Merge pull request #106 from RaidMax/bugfix/issue-0-misc-small-fixes
fix nuget package version for scriptcommands
2020-01-26 14:09:40 -06:00
451072276d fix nuget package version for scriptcommands
fix only one server being added during setup
2020-01-26 14:08:53 -06:00
39fb3b9966 Added support for the AC to PlutoT6
PlutoT6 requires pre-compiled GSC files.
Thats why I include the source and a compiled version. Since we can not create new GSC files but only can replace existing ones I did use this stock GSC to add our code to it.
2020-01-25 19:12:05 +01:00
e6bdcc9012 fixed server parser setup bug I was retarded about 2020-01-24 08:57:20 -06:00
9e345752f2 update parser selection menu text during setup
update IW4 script commands gsc and plugin to give base example
fix issue with new account alias linking (I think)
2020-01-21 18:08:18 -06:00
23f4e14244 Merge pull request #102 from RaidMax/bugfix/issue-0-misc-small-fixes
small change to detemine valid anticheat log lines
2020-01-20 11:57:57 -06:00
a53c2f5c44 small change to detemine valid anticheat log lines
fix new configuration generation bug as result of last pr
2020-01-20 11:49:56 -06:00
ad64540bb6 Merge pull request #101 from RaidMax/bugfix/issue-0-misc-small-fixes
web configuration changes for issue #100
2020-01-20 10:25:25 -06:00
697a752be0 make the version name match the actual name for FTP deployment
fix rare issue with summing session scores
copy font to expected wwwroot dir in debug mode so we get pretty icons when developing
upgrade some packages

pretty much reworked the entire server web config to support better validation and stuff.. not really a small fix

finish web configuration changes (I think)

finish up configuration changes and update shared library nuget
2020-01-20 10:23:23 -06:00
3a1cfba251 Merge pull request #98 from RaidMax/bugfix/issue-0-misc-small-fixes
fix issue with PT6 guid parsing in log file
2020-01-15 18:45:12 -06:00
7e3f632399 fix issue with PT6 guid parsing in log file 2020-01-15 18:43:52 -06:00
fa8dbe7988 finish version file upload (I think) 2020-01-15 15:41:09 -06:00
bdeb5b2408 let's actually look in the right folder for the version file 2020-01-15 15:25:34 -06:00
191bde9d1c test updating website version file from ftp upload 2020-01-15 15:06:05 -06:00
8afdb6df6f remove detailed version from csproj to hopefully force compile time set 2020-01-15 14:32:57 -06:00
a078da2715 round 2 of version injection 2020-01-15 14:06:29 -06:00
1c287ee354 testing new way to read in version hopefully injects now 2020-01-15 14:00:18 -06:00
2ae8fd6e5b test injecting build number 2020-01-15 13:32:01 -06:00
68deaec081 maybe let's not trigger a build now? 🤔 2020-01-15 10:21:10 -06:00
e737d990e9 update pipeline to only build on merge into release branches or (now previous standard 2.4-pr) 2020-01-15 10:18:02 -06:00
01198b66ea Merge pull request #97 from RaidMax/bugfix/issue-95-fix-restart-command
Bugfix/issue 95 fix restart command
2020-01-14 18:59:22 -06:00
943808562f fix error code page for things over than 404s
allow request token when not logged in
2020-01-14 18:56:23 -06:00
ec994d51be fix restart command (thanks .net upgrade)
reworking a little bit of stuff to allow depedency injection to start creeping in... it's coming
2020-01-13 20:06:57 -06:00
9be7bafc53 Merge pull request #96 from RaidMax/enhancement/issue-91-check-sv-running
implement feedback from issue #91 when sv_running is off
2020-01-13 16:54:38 -06:00
cd387ca08b implement feedback from issue #91 when sv_running is off
clean up a trying to abort thread which doesn't actually work with .net core
don't log event tasks cancelled as error, because it's not
2020-01-13 16:51:16 -06:00
639db5d7eb test powershell master version update
don't upgrade for this build
2020-01-12 16:46:39 -06:00
e64e02342e test master version update
don't bother updating for this build
2020-01-12 16:30:34 -06:00
51f91ede2c testing version push to master
no reason to update for this build.
2020-01-12 16:21:31 -06:00
009da92285 Merge pull request #94 from RaidMax/enhancement/update-client-master-versioning-and-include-userraw
make sure we force copy as directory
2020-01-11 21:03:45 -06:00
780b3459af make sure we force copy as directory 2020-01-11 21:01:51 -06:00
6acf1c67c1 Merge pull request #93 from RaidMax/enhancement/update-client-master-versioning-and-include-userraw
lets see if the game scripts copy now
2020-01-11 20:51:50 -06:00
27ad4fca43 lets see if the game scripts copy now 2020-01-11 20:50:46 -06:00
dd0d7192eb Merge pull request #92 from RaidMax/enhancement/update-client-master-versioning-and-include-userraw
updates to support new master versioning
2020-01-11 20:33:52 -06:00
8a42239f36 updates to support new master versioning
make sure game files are copied correctly in build output
2020-01-11 20:32:27 -06:00
23c78997ac Fix anticheat issue with needing index casting. IW you seem a little sloppy there... 2020-01-06 18:43:00 -06:00
7cdfe618a2 Add missing active columns with migration 2020-01-06 11:04:36 -06:00
7bfadca84d fix issue accessing the wrong logger 2019-12-29 17:07:00 -06:00
a7620ffd50 Merge pull request #90 from RaidMax/feature/webfront-help-index
Feature/webfront help index
2019-12-29 11:36:36 -06:00
902cd9953e finish help layout and show only on permission level 2019-12-29 11:32:36 -06:00
344c3613b8 add help page to layout 2019-12-28 20:44:39 -06:00
32e1af0ffb fix live radar directory and a few minimap names 2019-12-28 17:53:41 -06:00
1e1a03c9d8 fix retarded method that wasn't returning when it should have 2019-12-28 15:40:55 -06:00
4d3f7da48e re-enable API events 2019-12-28 10:07:37 -06:00
2b26b9a707 fix javascript libraries not being loaded because the stupid CDN change. MICROSOFT, IF YOU'RE LISTENING, FIX YOUR INTEGRATION WITH CDNJS THANK YOU
force demo record and increase max demos saved for IW4x
fix issue with disconnect on a not fully connected client
2019-12-27 20:37:50 -06:00
82381457df fix duplicate aliases from color codes (AB#5) 2019-12-27 14:42:17 -06:00
d4b5120953 Update azure-pipelines.yml for Azure Pipelines 2019-12-27 14:01:00 -06:00
82152755c9 add github release to pipeline 2019-12-27 13:45:53 -06:00
b251ef00c4 potential fix for a invalid operation exception on client update
change client library cdn provider as cdnjs seems broken at the moment
2019-12-27 12:10:20 -06:00
042fde971e (potentially) fixed object disposed issue with semaphore
fix random issue where we were trying to reset a session for a player that has not fully connected
2019-12-26 18:17:49 -06:00
c9e6ce0bca fix authorize issue on penalty info after upgrading .NET Core runtime targets 2019-12-25 21:05:57 -06:00
c4df53c195 fix issue with script plugins not reloading (AB#2)
fix issues with collation on MySQL (AB#1)
2019-12-25 14:32:57 -06:00
3a06b3862d Update projects to .NET Core 3.0
Increase max sv_hostname length on master
2019-12-24 15:23:43 -06:00
9fa5de1418 woo it works, now we have a reasonable output filename 2019-12-09 14:17:12 -06:00
a9e7d2d314 Maybe this will work better 2019-12-09 14:08:52 -06:00
b3f4712dd2 update the json output encoding 2019-12-09 14:01:02 -06:00
e5d009d87d merge from 2.3 2019-12-09 13:30:50 -06:00
70ca202889 Test json generation of version info 2019-12-09 13:28:20 -06:00
b8aa8a0b4d reeee 2019-12-07 20:42:30 -06:00
1f755f535c I'm retarded 2019-12-07 20:28:50 -06:00
f37e954e2f third time's the charm? 2019-12-07 20:20:44 -06:00
31bcd52c79 lets try again 2019-12-07 20:11:22 -06:00
72f3a51657 run version grab after publish 2019-12-07 20:00:03 -06:00
3b4af20810 grab version information from file to setup output zip name 2019-12-07 19:52:03 -06:00
890c419133 Fix color code tag helper not being loaded 2019-12-07 10:49:40 -06:00
f9680971af Update to build solution instead of individual projects 2019-12-05 17:09:35 -06:00
5df9332d4c Update azure-pipelines.yml for Azure Pipelines
Make open-iconic directory if not exists (for real)
2019-12-05 16:32:23 -06:00
4379d04b00 Update azure-pipelines.yml for Azure Pipelines
Make open-iconic directory if not exists
2019-12-05 16:31:40 -06:00
9d639097d3 Merge pull request #86 from RaidMax/dependabot/pip/Master/werkzeug-0.15.3
Bump werkzeug from 0.14.1 to 0.15.3 in /Master
2019-12-05 16:20:28 -06:00
cade2242bf Merge pull request #87 from xerxes-at/2.3
Fixed the PT6 parser
2019-12-05 16:20:10 -06:00
1d7377f975 Fixed the PT6 parser
Reworked most of the regex.
Fixed the mapping.
This hopefully fixes all issues with it.
2019-12-04 20:08:01 +01:00
e7395f02ce Bump werkzeug from 0.14.1 to 0.15.3 in /Master
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 0.14.1 to 0.15.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/0.14.1...0.15.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-04 02:00:44 +00:00
c9e2a11745 Update azure-pipelines.yml for Azure Pipelines 2019-12-03 16:28:51 -06:00
9db38a130c fix stat controller build plugins in correct mode 2019-12-03 16:27:26 -06:00
25a69a2018 don't use temporary table on mysql migration as it breaks 2019-12-03 15:56:00 -06:00
98c4a700a2 merge 2019-12-02 15:56:30 -06:00
3defd3f486 move all the deployment setup into 2.4 pr (#85)
* don't run build commands in release

* fix test file

* Set up CI with Azure Pipelines

[skip ci]

* Include fonts and fix automessage hidden command

* more project changes

* migration from bower to libman

* more lib man changes

* project update for sneaky commands

* add missing canvas.js dep
update projects not to have stupid extra dlls

include in previous

* update pipeline file

* update post publish script and pipeline definition

* fix broken yaml

* move encoding conversion to seperate script

* remove extra uneeded rank icons
remove garbage language files being created
remove frontend lib when done

* fix publish script path

* grab localizations through powershell

* fix broken batch 🤷

* actually fixed

* only include runtime compilation in debug mode for webfront

* don't deploy un minified css
use full jquery version

* add step to download the scss for open iconic
change the font path

* update mkdir for iconic path

* don't include old iconic css

* correct font path for real now

* copy script plugins

* lots of changes for deployment

* build the projects

* use projectdir instead of solution dir

* nerf script commands plugin
fix live radar left over command

* actually kill script command post build

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* fix the font file copy (I think)

* maybe fix delete folder issue

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines
2019-12-02 15:52:36 -06:00
b086190ab0 renable weapon name in anticheat snapshot list
update migrations for unique index
fix missing total connection time
include total connection time in get client query
2019-11-25 12:05:12 -06:00
56008e80c7 update mapname from status query 2019-11-18 14:02:35 -06:00
0ac1a4f861 Merge branch '2.3' into 2.4-pr 2019-11-18 13:07:24 -06:00
fb6d20e214 fix regex pattern for PT6 2019-11-18 12:29:37 -06:00
a9f6106c6e fix silly mistake with trying to assign something to an object that could be null 2019-11-18 09:25:39 -06:00
d1886fdd20 Fix small issue with query optimization missing a FK set
Fix accidentally rename of controller method
2019-11-18 08:08:09 -06:00
161b27e2f2 fix alias command sending message to origin instead of target
(hopefully) fix an issue with banned players causing exception if they create events before they are kicked out
fix issues with sometimes wrong error message for timeout
show most recent IP address at top of alias list
optimization to some sql queries
2019-11-15 14:50:20 -06:00
cb9119ac58 add more informative 404 errors 2019-10-23 13:35:20 -05:00
f31ce6b001 allow enabling of only specific detection types
allow override of anticheat for tmw3
fix invalid cast if E.Extra is not a command
add a delay after map rotation before getting the the server info. (hopefully prevents increased lost connection notification frequency)
2019-10-23 10:40:24 -05:00
96e434213f refactor some event handling
add concept of blocking events
2019-10-18 13:39:21 -05:00
b992f4d910 add unlink command
fix parsing names with colors codes enabled
2019-10-11 15:26:13 -05:00
6b27beb355 update mysql provider to pre release so it works with .net core 3.0 2019-10-10 13:15:31 -05:00
4872e2e8c4 fix issue with top stats query and client evaluation 2019-10-09 18:51:50 -05:00
a37524c726 fix small exit exceptions
fix the live radar tab switching for .net core 3.0
change events to use "sequential" but still parallel
update the publish scripts
2019-10-09 15:51:02 -05:00
6cd3879bac Merge 2019-10-08 17:02:22 -05:00
c630f65317 update the project files even though the culprit was actually the publish file 2019-10-08 16:47:36 -05:00
76cfe30c0f update version number 2019-10-07 17:39:17 -05:00
a7872aaffd ensure that demoted clients are logged out from the webfront 2019-10-07 17:35:37 -05:00
4635d85ff8 forgot an else in a migration 2019-10-07 10:43:44 -05:00
068e943fd3 update values for snap and offset
fix some issues from .NET Core 3.0 upgrade
2019-10-07 10:26:07 -05:00
c4e0b0272c update packages 2019-10-02 18:58:23 -05:00
ede5c9de51 update recently clients to show last 24 hours
fix color codes on profile meta data
2019-10-02 17:59:10 -05:00
524589717b Update to .NET Core 3.0 2019-09-30 18:35:36 -05:00
88af032736 Update shared GUIDs 2019-09-30 13:00:44 -05:00
260a8800a4 prevent raw html when color codes are enabled 2019-09-28 19:13:30 -05:00
37261c9a54 update some anticheat code 2019-09-27 15:53:52 -05:00
5073ec39bf Merge branch '2.3' into 2.4-pr 2019-09-27 15:51:57 -05:00
3af9f55bf1 Fix ordering of admins by level, then name 2019-09-27 15:49:03 -05:00
f7cbf73c44 Merge branch '2.3' into 2.4-pr 2019-09-26 16:11:58 -05:00
1e9a87d6fa prevent penalties from being lost in edge case alias linkage
small optimization with tasks
2019-09-26 16:08:49 -05:00
fe6fe39800 don't group admin list by alias id 2019-09-14 17:22:47 -05:00
082776aca5 prevent "laggy" angles from being tracked 2019-09-10 17:50:23 -05:00
483b7917ac Merge branch '2.3' into 2.4-pr 2019-09-10 17:26:48 -05:00
7f7353c505 only count hits for valid recoil detection 2019-09-10 17:24:32 -05:00
adc73eb7ff merge from 2.3 2019-09-09 17:41:58 -05:00
c18be20899 add snap metric to anticheat
update various small code bits
2019-09-09 17:40:04 -05:00
198f596ab3 small updates to stat handling
various little tweaks
2019-09-09 17:37:57 -05:00
148d28eaca Merge branch '2.3' into 2.4-pr 2019-08-30 17:52:35 -05:00
58a73e581f fix rare issues when converting encodings
add readme for AC
2019-08-30 17:24:44 -05:00
2d9b6b8394 prevent privileged client from being flagged when reported
fix issue with enum parsing on finding client
2019-08-30 13:31:23 -05:00
aa9dd7ac6d Merge branch '2.3' into 2.4-pr 2019-08-30 11:50:48 -05:00
47d5df1aa1 bump application version 2019-08-30 09:29:19 -05:00
d644387091 Strip out color codes and spaces when checking for min length 2019-08-30 09:26:16 -05:00
db3a20c60b merge from 2.3 2019-08-28 13:47:38 -05:00
27a05ce6db update api controller to support actually filtering events by server
fix up stats manager async semaphore wait
add new shared guids
fix regex parsing with empty name
2019-08-28 13:45:53 -05:00
7c0e37cc8e Merge branch '2.3' into 2.4-pr 2019-08-24 20:16:35 -05:00
11d2df1fe8 small stat changes 2019-08-24 20:15:50 -05:00
a820929582 another fix because I'm retarded
bump version up
2019-08-24 14:06:23 -05:00
dcd1c97d37 Merge branch '2.3' into 2.4-pr 2019-08-24 11:10:43 -05:00
6726217354 Make stats update after 10 kills so we don't wait quite as long
Gracefully disconnect clients on shutting down again
2019-08-24 10:02:53 -05:00
c1a825f8f2 Merge branch '2.3' into 2.4-pr 2019-08-23 21:28:09 -05:00
563c73221e actually fix it here 2019-08-23 21:27:36 -05:00
d35001049f Merge branch '2.3' into 2.4-pr 2019-08-23 19:12:28 -05:00
652f3fb86b Fix small issue with saving client kills multithreaded 2019-08-23 19:11:36 -05:00
f877ba73a9 Merge branch '2.3' into 2.4-pr 2019-08-23 18:36:28 -05:00
91078eec0f Update to some stat stuff to fix some latent issues 2019-08-23 18:34:31 -05:00
a57c982270 Merge branch '2.3' into 2.4-pr 2019-08-18 11:20:31 -05:00
ed5d8faf5c tweak for showing the generated graph color properly in other browsers.
apparently the "style" hack doesn't work, but using "title" does
remove return in customcallbacks
2019-08-18 11:20:19 -05:00
f6857ac635 bugfix for issue #81 (linked accounts being demoted) 2019-08-18 11:18:20 -05:00
320b01d15c Merge branch '2.3' into 2.4-pr 2019-08-12 20:09:31 -05:00
001ecc5961 prevent flagging banned players 2019-08-12 20:00:40 -05:00
4bdd240122 update callback
set player color history to the correct darken amount
2019-08-12 19:04:25 -05:00
5fef69d697 slight tweak to log reader to expire old keys 2019-08-10 17:58:20 -05:00
8fc85ef4c1 have graph color generated by css so that MS Edge doesn't freak out when using rgba
don't do simple word check on offensive name
2019-08-10 17:35:34 -05:00
85d88815f1 top stats info is per server instead of total when selecting each tab
fix issue with ingame name failing to match when using color codes
only show live radar for servers that support it
2019-08-10 09:08:26 -05:00
a0266c5e69 Merge branch '2.3' into 2.4-pr 2019-08-08 15:59:00 -05:00
2ba0b1e7d3 prevent same level clients from demoting each other 2019-08-08 15:58:23 -05:00
3051d44b0d show trigger regex for profanity determent plugin 2019-08-08 15:30:06 -05:00
b8a310bb07 prevent flag icon from showing on banned profiles
implement automated penalty info for profanity determent issue #75
2019-08-06 13:36:37 -05:00
d11a5f862b add missing dragunov to the live radar weapons
color code process names in chat context
2019-08-04 21:25:56 -05:00
08d250156c fix login issue
strip colors for logging
feature implementation for issue #76
2019-08-04 20:38:55 -05:00
75378400e7 Add flag icon on client profile 2019-08-04 17:06:07 -05:00
bb42861a92 finish color code support (I think) 2019-08-02 18:04:34 -05:00
dfecb99d07 Merge branch '2.3' into 2.4-pr 2019-08-01 19:45:44 -05:00
1c66ac9117 fix issue with log reader
fix issue with searching names on webfront that could be parsed to hex
2019-08-01 19:42:44 -05:00
55fb36863c fix copy paste error in penalty loader
start allowing color codes from ingame
2019-08-01 09:37:33 -05:00
034d887abd modify how reading from file works to prevent accidental overreads 2019-07-31 20:15:29 -05:00
9f3f344daa add a bit more logged for when live radar fail to update
update killhouse map offsets (it's still wrong though)
2019-07-29 12:08:25 -05:00
ebe85a9ded Merge branch '2.3' into 2.4-pr 2019-07-27 17:50:25 -05:00
06af995202 prevent certain shotguns, and shotgun attachments from being used for no recoil detection 2019-07-27 17:46:48 -05:00
92e71ae2f4 finish custom accent color feature 2019-07-27 15:23:45 -05:00
f8505781a0 fix issue with teknomw3 GUIDs 2019-07-27 10:02:46 -05:00
3b9b99a07e start work to allow custom accent colors 2019-07-27 08:18:49 -05:00
ab4ce41015 Merge 2.3 into 2.4-pr 2019-07-26 10:25:05 -05:00
f613f0aace finish tweaks to log reader
add some more shared guids
2019-07-26 10:22:02 -05:00
2b8d8fc4b7 Merge 2.3 into 2.4-pr 2019-07-25 10:01:20 -05:00
ac32034910 optimize index for rating history
update log server to prevent delays or missed information
2019-07-24 19:15:07 -05:00
9665d2d457 fix issue with duplicate js function names for loader
hide flagged status of users on webfront unless logged in (will still show the level if they report someone because cba to update the view component w/out auth status)
add terminal to the radar maps
2019-07-24 10:36:37 -05:00
d73d68d9f4 increase master history to 7 day, up from 1 day 2019-07-21 17:14:44 -05:00
50ba71c6fb small code cleanups 2019-07-19 14:54:39 -05:00
38f1169061 finished server selection for live radar and adding it as button to home screen
only update flag for recent players if country code is available
2019-07-19 10:33:00 -05:00
5c90228320 Move folder structure for radar plugin 2019-07-17 13:26:48 -05:00
03db194046 Add unstaged files 2019-07-17 13:16:45 -05:00
68382d3f61 Remove double track images 2019-07-17 13:16:25 -05:00
4e99046874 merge 2019-07-17 13:09:25 -05:00
64b320614b add images for radar to source control
cleanup some nuget packages
2019-07-17 13:00:30 -05:00
7b5f3e8e83 move some stuff for live radar for compiled views
add chat icon to send messages to servers on server view
2019-07-17 12:38:02 -05:00
748841776f More radar tweaks 2019-07-17 12:38:02 -05:00
edfbb92a3f can you say more radar updates? 2019-07-17 12:38:01 -05:00
1a9a0e48b7 lots more live radar updates 2019-07-17 12:38:00 -05:00
d27f1ded36 tweak initial live radar 2019-07-17 12:38:00 -05:00
e5cd824c99 start work for live radar 2019-07-17 12:37:24 -05:00
2542b7de12 Clean up some old files 2019-07-17 12:29:51 -05:00
f42a66e756 add most recent players dropdown option to webfront
remove unneeded compiled bootstrap file
2019-07-16 15:27:19 -05:00
d301915273 Merge 2019-07-13 20:50:10 -05:00
fc43e47874 move some stuff for live radar for compiled views
add chat icon to send messages to servers on server view
2019-07-13 20:45:25 -05:00
b0365a5a43 More radar tweaks 2019-07-08 20:21:20 -05:00
2a63a55359 can you say more radar updates? 2019-07-08 20:21:19 -05:00
0e9fd144f1 lots more live radar updates 2019-07-08 20:21:18 -05:00
d81646087e tweak initial live radar 2019-07-08 20:21:17 -05:00
7f1da4d1fc start work for live radar 2019-07-08 20:21:17 -05:00
68f6be23a6 require minimum kills before recoil threshold evaluated
make sure edit configuration link on webfront visible when accessing via localhost
2019-07-07 17:57:06 -05:00
0b282b2664 lots more live radar updates 2019-07-05 20:53:03 -05:00
665218f641 tweak initial live radar 2019-07-02 17:30:05 -05:00
b64bce2936 start work for live radar 2019-06-30 13:39:40 -05:00
042327840f fix bug with wrong locale when master is down
fix issue with reapplying penalties
show subset of penalties that are linked on client profile
2019-06-28 16:57:01 -05:00
3d468e32b9 clean up some penalty stuff
force log file to be written if none supplied
fix issue with not all meta loading
2019-06-27 20:06:30 -05:00
16d2ec82b8 make sure flags are excluded from active penalties on player profile
modify how flags "expire"
2019-06-25 18:01:47 -05:00
421e90cf70 fix old bug of auto unflag not working
fix wrong thresholds on recoil
2019-06-24 18:32:14 -05:00
8119ff9f83 adjust detection thresholds for recoil and offset
make sure we don't keep adding penalties after first
add "Other" penalty for future plugin use
2019-06-24 16:56:47 -05:00
253c7c8721 allow reports to be filed against anyone
fix rare issue with alias (maybe)
update some tests
2019-06-24 11:01:34 -05:00
cb80def122 update version
make sure ac snapshots are saved
2019-06-16 14:49:04 -05:00
e669d0be82 don't count bots on master list
don't save every ac snapshot oops..
2019-06-16 12:19:23 -05:00
495197c19d add no recoil detection 2019-06-15 17:37:43 -05:00
a5414c2c57 Merge branch '2.3' of https://github.com/RaidMax/IW4M-Admin into 2.3 2019-06-15 08:53:15 -05:00
cbfb3919fc fix GUID parsing on T6 2019-06-15 08:52:59 -05:00
d789542d0f Update README.md 2019-06-14 18:48:14 -05:00
c6c2ec7784 fix start scripts on linux (dos2unix)
fix permissions on linux (why were/are they carrying over from windows? )
2019-06-14 18:16:47 -05:00
4645bd84e8 prevent partial client updates from setting things they shouldn't be *cough* mask *cough*
setup shared library for NuGet package
fix a couple things with offset detection calc
get cod4x working again
2019-06-13 19:10:08 -05:00
10829b32ad update anti-cheat offset calculation 2019-06-12 10:27:15 -05:00
e86904b11e add a check to make sure we're not breaking EFClient entries when updating
make sure the alias is updated before banning the player as we want to link them together
update CoD4x parser to fix their breaking change
2019-06-11 08:00:14 -05:00
82390340c9 fix duplicate meta data when restarting
fix issue with parsing anticheat info in non en-US culture
fix rare issue with client spots "swapping"
don't copy referenced shared library assemeblies from plugins
2019-06-09 09:50:58 -05:00
163523d586 convert GetPort to auto property
don't force disconnect player if someone is "in" their spot
increase gamelogserver max time before purge
2019-05-31 10:17:01 -05:00
95d64df321 combined Penalty and EFPenalty
moved some classes around
2019-05-29 16:55:35 -05:00
0b0290a871 fix issue with restarting via web
replace some hard coded string in javascript with localization
break things to fix things
2019-05-17 09:02:09 -05:00
5f588bb0f7 clean up the profanity determent plugin by using the Get/Set Additional properties
cleaned up the base event parser to not need the server object to generate the event
Hopefully prevent anticheat from freaking out when database connection is lost
2019-05-13 10:36:11 -05:00
b99cc424e7 fixes for things that should have been in the previous release
console output reenabled
server start task actually runs
2019-05-09 20:00:00 -05:00
1dc0f5a240 fix aggregate issue with KDR on global top stats
refactor some of the main application code to have a cleaner code flow
add enviroment flag to opt out of .net core telemetry in start script
fixed "a moment" missing the "ago"
fixed case sensitive client searches on postgresql
clean up command code flow
Add missing map "mp_cairo" to default settings
2019-05-08 20:34:17 -05:00
43c4d4af38 force bots to all use the same profile
use C# 7.1 for projects
2019-05-04 09:17:18 -05:00
db11a5f480 upgrade packages, and delete a few unneeded ones
fix search for client resulting in invalid GUID parse
simplify output from dvar not being found
make sure to prompt if not all servers could be reached
2019-05-03 20:13:51 -05:00
b51af7ca9a fix penalty list javascript loading duplicates
make bad GUID parse throw an exception so we don't have a client connect with GUID of 0
no longer print out ac debug messages
fix small issue of trying to parse empty chat messages
fix issue with set level on accounts with multi guid, same IP
2019-05-02 22:33:38 -05:00
2cceb2f3e7 make database seed code less verbose
disable killserver command
fix issue with default parser not saving during setup
fix issue with unban reason displayed when player is rebanned
2019-04-28 20:54:11 -05:00
599a14b646 optimize the find client query 2019-04-25 21:05:35 -05:00
02622ea7de update application version
ignore stat events of bots if they are ignored
limit max number of bot profiles to 18, greater than 18 wraps
prevent anti cheat from running on bot events
create localization folder on publish so copying over doesn't fail
include quick message mapping on webfront server history chat
make gravatar on profile not repeat
2019-04-25 13:00:54 -05:00
03ae3b5822 deleted localization files as they're now generated during release
reintroduce throttling for servers that are unreachable (defaults to 60 seconds between polls)
small revert to the RektT5M parser contell -> tell
add migration to introduce gamename to server
impllement quickmessage mapping
2019-04-23 17:27:20 -05:00
0711249a46 add parser for RektT5M
update base event parser to include "short" GUIDs
updated some localization
add tooltip to anti cheat metrics on profile for more information about what they mean
2019-04-21 16:28:13 -05:00
18f4ffa9ff fix extra prompt for server port
change vpn detection failure to warning instead of error
2019-04-18 14:19:50 -05:00
934fead5c2 fix issue with login 2019-04-17 17:50:53 -05:00
02cad10d77 prevent starting if no servers can be connected to
fix nextmap issue on t6
fix bug with kicking client for profane name
2019-04-16 11:32:42 -05:00
b134cd4728 fix gravatars not showing
fix web config not saving Uri
fix issue with token login
2019-04-14 10:55:05 -05:00
b9c4a1b5f6 finish initial implementing application configuration editing through webfront
todo: server configuration
2019-04-12 22:25:18 -05:00
f0fd4c66e9 add configuration option to force local translations
fix silly bug with no being able to claim ownership
continue work on configuration via webfront
2019-04-11 20:43:05 -05:00
52fe8fc847 !setgravatar uses meta service now
update certain prompts to use interpolated strings from translation
update application version
2019-04-09 15:02:49 -05:00
9f8c35dbed fix bug with chat context timestamps not parsing is different machine locales
add disallowed client names to default config
fix ping not working for targets
2019-04-08 20:31:32 -05:00
9d9be7f8af few more small fixes
complete join button on webfront
update for 2.2.6.0
2019-04-08 12:29:48 -05:00
dd82a5e3fa start add of join button (still need to grab the external IP address)
finish up fixes for alias stuff
2019-04-07 20:14:59 -05:00
8667532d24 remove create proxy as it's not even used anymore
more fixes for alias stuff
hopefully fix rare bug where client activity cshtml loop goes oob
add URLProtocol format to event parsers to allow connecting through webfront
2019-04-06 21:48:49 -05:00
863ba8b096 strip drive letter on gamelog server if running on linux
strip undecodable chacters from gamelog server log file
finish re work on alias add/update ( I think)
2019-04-05 21:15:17 -05:00
8ab89e113d clean up log reader/make it output more useful message if things go wrong
add unflag as a penalty
show bans/tempbans even after they've expired on penalty list
continue making alias links great again
2019-04-05 13:34:03 -05:00
00634780d4 use "world client" when recieving fall damage/damage
fix rare bug with GetClientByName
refactor some alias/ef stuff. still more to do
2019-04-02 20:20:37 -05:00
6f80f1edbb refine webfront pages
finish refactor of penalty information/profile
optimize pull penalty query
start impl of quick message mapping
2019-03-31 19:56:31 -05:00
9393b35c39 start implementation of configuration via webfront 2019-03-30 22:04:15 -05:00
37d3f4f90d changes for latest release 2019-03-30 17:21:01 -05:00
8521df85f5 finish initial rework of profile page with meta pagination 2019-03-29 21:56:56 -05:00
7b8126d57e continue rework of profile
start moving profile info out of javascript into componentview
rework meta data to include count and offset
2019-03-27 19:40:26 -05:00
1e2e2218e3 finish initial rework of profile page 2019-03-26 21:02:11 -05:00
11e3235d5d fix issue with not loading last connection for admins
continue work on fixing profile
2019-03-25 21:12:16 -05:00
cae6d8389e fix bug with privileged users not always showing the most recent profile
temporary bans are now applied to all linked accounts instead of a per-guid basis
rework set level flow
add guid and ip address (if logged in) to public async endpoint
2019-03-24 21:34:20 -05:00
1056c7c335 fix for issue #70
update start script for windows
2019-03-18 10:36:31 -05:00
95e4ee672e Merge branch 'master' of https://github.com/RaidMax/IW4M-Admin 2019-03-17 17:38:17 -05:00
bf0a0befc6 fix top players row not fill full width
make stats web use view imports
add more games/maps to default settings (thanks to FryTechTiddys
#7622)
replace ConfigurationBuilder Config Handler implementation with Newtonsoft.Json as it wasn't deserializing "complex" classes properly
2019-03-17 17:37:50 -05:00
53c3ff6ce3 2 typos fixed
Plutoniun -> Plutonium
IW3 -> IW4
2019-03-17 08:06:15 -05:00
c8ec0eefa9 game log reader reads async now.
should have done that a long time ago
update profile page to have a bit better space usage
2019-03-09 10:28:04 -06:00
82bae772f0 most played command now ordered by play time
issue #68
2019-03-02 17:29:09 -06:00
af3aea77bc finish UI tweaks for issue #39 2019-03-02 14:29:09 -06:00
b3e5f468a1 finish implementation of per server top stats page 2019-02-27 20:13:15 -06:00
c21bf2ebf1 continue working on per servver topstats 2019-02-26 21:25:27 -06:00
d318a57830 start implementation for per server top stats
issue #39
2019-02-25 19:36:10 -06:00
61f1436faf prompt user to continue if not all servers can be connected
issue #58
2019-02-24 19:35:59 -06:00
0c527a5f65 Add lock menu icon for tempbanned players
issue #62
2019-02-24 19:07:56 -06:00
1457b843e2 one more small fix for meta service after testing live 2019-02-22 19:37:53 -06:00
de69bed792 small migration fix for MySql 2019-02-22 19:35:03 -06:00
4b1f44cc2a re-enable login to webfront with password
update cookie to last 3 months
add configuration option to limit # of rss feed items
prevent database tracking of bots if ignore bots requested
add last map and last server played to profile
2019-02-22 19:06:51 -06:00
74cdf8e885 Fix bug introduced with auto messages 2019-02-19 19:39:09 -06:00
5d41059641 accidentally copied a file to the wrong project 2019-02-18 19:48:28 -06:00
2e6889d9bb implement RSS feed in auto messages for issue #53
modified automessages to use async mesthods instead of synchronous
2019-02-18 19:30:38 -06:00
9c4d23f0b4 enhancement for issue #63 2019-02-17 18:48:40 -06:00
b4e3e8526a fix for issue #66 2019-02-17 13:16:48 -06:00
e3944fb8c2 add web project for stats to fix bug with pre compiled razor templates 2019-02-16 17:18:50 -06:00
40f1697c97 fix damage event not including log line
complete initiall implementation for "2FA"
issue #52
issue #66
2019-02-16 15:04:40 -06:00
2bbf2988da update application version 2019-02-15 22:20:45 -06:00
5e36bf4316 begin implementation of token authentication
replacing password authentication ingame
precompile views for webfront
issue #66
issue #52
2019-02-15 22:19:59 -06:00
a362caebac fix for T6 guid length including sign 2019-02-14 08:42:14 -06:00
2260d8974d update master to allow IW5 to pass validation
include version set on manual parser selection
update projects to .NET Core 2.2
add middleware to support ip whitelisting
(EnableWebfrontConnectionWhitelist and WebfrontConnectionWhitelist)
issue #59
2019-02-12 20:34:29 -06:00
5e04274da6 actually fix the encoding issue 2019-02-10 20:05:50 -06:00
6d2e6aee4f update application version 2019-02-09 21:26:39 -06:00
9e74c42246 fix small bug with log paths 2019-02-09 21:24:54 -06:00
dea5b3f954 fix reading PT6 having signed decimal GUID in log
fix  alternative encoding character converting
allow more paths for game log server
add localization for unknown ips in welcome plugin
add gamelog server uri to support game log server on games that must supply manual log path
misc fixes
2019-02-09 15:35:13 -06:00
7c6419a16a finish cod4x parser
add IW3 map names
2019-02-06 20:12:35 -06:00
0194196a33 update application version 2019-02-05 18:06:14 -06:00
044991272f update parsers to include game name
prompt to enter log path if game doesn't generate
2019-02-05 18:02:45 -06:00
f3290cf066 move IW4x parser out of code
add CoD4x parser
2019-02-05 11:14:43 -06:00
29eedea093 fix IW4x regression error with alternative encodings
add parser selection to server config setup
2019-02-04 19:38:24 -06:00
ce02f5dd68 Move T6 parser to javascript parser 2019-02-03 20:47:05 -06:00
e6bfa408f8 move IW3 parser to javascript 2019-02-02 20:19:24 -06:00
0a1dc46760 Add commenting for parsers
rename IW4*Parser to Base*Parser
2019-02-02 19:40:37 -06:00
97ba6aae2e put parser in right location :P 2019-02-02 19:13:17 -06:00
a456fab0e5 Increment version #
Add TeknoMW3 parser file
2019-02-02 19:11:34 -06:00
3e5282df87 Finish preliminary parser for TeknoMW3 2019-02-02 18:54:30 -06:00
59e0072744 Finish dynamic dvar parsing for IW4x 2019-02-01 19:49:25 -06:00
f1dd4f7c7f Fix IP parsing bug introduced with IW4Parser
Additional fix for Webfront Index OoB on _ClientActivity
2019-01-28 18:21:56 -06:00
760d3026ce Fixes for PR 2.3.4.0 2019-01-27 19:45:35 -06:00
07df6dbf79 Update version number and small plugin fix 2019-01-27 18:54:18 -06:00
ca535019c6 Finish RCON dynamic parser impl
Fix configuration generation bug
2019-01-27 18:41:54 -06:00
e6154822f6 Implement more dynamic parser stuff 2019-01-27 16:40:08 -06:00
7a6dccc26a Fix bug with webfront spamming issues when running
Remove IW5 parser
Begin implementation of dynamic parsers
2019-01-26 20:33:37 -06:00
08c883e0ff fix duplicate bot welcomes
fix prompt bool incorrect default value
rename GameEvent.Remote to GameEvent.IsRemote
include NetworkId in webfront claims
fix non descript error message appearing when something fails and localization is not initialized
2019-01-03 14:39:22 -06:00
aaf9eb09b6 more alias changes :(
fix flag penalty coming from wrong user
2019-01-02 18:32:39 -06:00
7b75c35c9b fix aliases for real (hopefully)
fix bug with flag not being applied
fix level being set based on IP instead of IP and name
2018-12-31 20:52:19 -06:00
07ec5cf52f update assembly version 2018-12-30 20:52:26 -06:00
cf5ee8765d finish alias fixes
add manual webfront bind url
2018-12-30 20:48:07 -06:00
9494a17997 update prompt utility functions for issue #65
tweaks to alias stuff
fix bug with bots not showing
2018-12-30 18:13:13 -06:00
5f4171ccf4 hopefulyl fix aliasing issue
bans are applied to an account if the accounts are linked but penallty on a different accounts
2018-12-29 12:43:40 -06:00
a10746d5ff Fix small issue with log reading 2018-12-19 19:24:31 -06:00
8dca05a442 Small fixes 2018-12-17 13:45:16 -06:00
8aa0d204f4 Delete stupid 2018-12-16 21:52:11 -06:00
12cf2e8247 Add server version to master api
Add IsEvadedOffense to EFPenalty
Fix remote log reading in not Windows
2018-12-16 21:16:56 -06:00
b77bdbe793 minor fixed 2018-12-03 19:21:13 -06:00
4522992c0e fix remote commands
user clientkick_for_reason for T6 parsers
small bug fixes
2018-12-01 12:17:53 -06:00
9d6cbee69c update stats
change server id
fIx change log server complaining when empty read
2018-11-27 18:31:48 -06:00
abf0609e2e fix only one administrator showing on admins page
fix profanity determent not applying penalties.
2018-11-25 21:11:55 -06:00
5ac8a55c72 fixes for new polling setup
update database model for alias (nullable ip)
heartbeats now send ip to master server
2018-11-25 20:00:36 -06:00
9bdd7d1b8a More work modifying client stuff 2018-11-07 20:30:11 -06:00
ed83c4c011 started work on getting the restart functionality in the gamelogserver
fix bug with unbanned players still showing as banned via lock icon
move player based stuff into client class
finally renamed Player to EFClient via partial class
don't try to run this build because it's in between stages
2018-11-05 21:01:29 -06:00
d9d548ea18 Small anti-cheat update 2018-10-28 20:47:56 -05:00
1779bf821d more work on skill based team balance.
added on player disconnect to custom callbacks
2018-10-25 08:14:39 -05:00
d50e6c8030 change penalty expiration datetime to null for perm bans
add tempban max time
allow searching for GUID
stats returns ranking as well
fix for promotion/demotion text
2018-10-15 19:51:04 -05:00
a58726d872 add gsc api controller for communicating with gsc
add ignore bots option
fix first localization message not working
2018-10-13 18:51:07 -05:00
dded60a6ef add test to print out all commands 2018-10-12 21:32:30 -05:00
305817d00c version 2.2 stable 2018-10-12 21:28:22 -05:00
65cf3566db fix publish for release
fix bug with localization issue crashing app if config is wrong
2018-10-12 18:59:17 -05:00
e91d076b41 curtail lost connection messages from RCon
verify still compatible with T6
fix potential null reference exception during configuration reading/setup
2018-10-10 19:22:08 -05:00
b5e9519f0c update shared guid kick plugin
script plugins reload without error using the correct file share mode when opening
increase socket timeout to 10 seconds
2018-10-09 20:19:06 -05:00
7caee55a7e refactored the welcome plugin to use a web api instead of a hard coded file
removed deprecated file class
don't wait for response when setting dvar/sending command in T6
potential fix for duplicate kick message in JS plugin
2018-10-08 21:15:59 -05:00
b289917319 update project to .net core 2.1.5
got rid of "threadsafe" stats service in stats plugin
2018-10-07 21:34:30 -05:00
c8366a22e5 fixed the vpn detection plugin method signature call
added some fixes for stats/ac
2018-10-06 15:31:05 -05:00
de902a58ac write individual server log files and main log file seperately
log writing is thread safe now
2018-10-06 11:47:14 -05:00
c7547f1ad5 clean up publish folder output to have a less cluttered structure.
add migration class to perform the migration on update
2018-10-05 22:10:39 -05:00
9d946d1bad more stability changes 2018-10-03 21:20:49 -05:00
f4ac815d07 hopefully finished with RCon changes.
added more tests.
fixed issues from event changes (there's most definitely still issues related to that)
2018-10-02 12:39:08 -05:00
7fa0b52543 more rcon tweaks, and starting on unit tests for commands bleh 2018-09-29 21:49:12 -05:00
d45729d7e1 clean up rcon, fix a bunch of little things 2018-09-29 14:52:22 -05:00
5d93e7ac57 a ton of stuff and fix migations 2018-09-23 19:45:54 -05:00
0f9d2e92e1 fix for issue #50 2018-09-16 17:51:11 -05:00
4a46abc46d add index to time sent in EFCLientMessage, so we can retrieve faster in context view
set the maximum height of the
add link to profile on client chat
move change history into a seperate service
move around AC penalty processing
2018-09-16 15:34:16 -05:00
7c708f06f3 adds two day ban to drop down on for issue #47 remove IW5 (Pluto) from supported client until it's rewritten 2018-09-13 20:00:41 -05:00
98adfb12d2 update map names for IW4 (issue #48)
only check shared GUID for IW4
optimized get privileged clients query
fine-tuned the version printout to include revision numbers
2018-09-13 14:34:42 -05:00
a786541484 re-implemented auto-upload on publish
fixed the max length migration for MySQL
configure the python projects to be able to be published from command line
optimize find active pentalties query
add feature for issue #38
testing fix for concurrent dict access (in stats plugin)
2018-09-12 19:53:11 -05:00
b9086fd145 fix parsing view angles in exponential form
update RestEase  and CodePages dependencies
optimized the find by name query
add index to name
2018-09-11 14:28:37 -05:00
3d8108f339 fixed profanity bug
fix the shared GUID connect
fix linux  log issue
2018-09-08 20:20:11 -05:00
39596db56e fixed rating and kill streak bug, but uncommenting something I forgot I commented out
Added SharedGUIDKick plugin to kick people with shared GUID
2018-09-08 17:29:30 -05:00
ba5b1e19a6 update readme
add vision average to client stats
other stuff
2018-09-07 22:29:42 -05:00
385879618d add game log server 2018-09-06 13:25:58 -05:00
0c90d02e44 update libraries to pre release
fix remaining issue for issue #32
adds overall ranking to profile page for issue #24
2018-09-04 21:07:34 -05:00
cfbacabb4a fix bug with player not getting updated on disconnect (related to issue #24)
jint version downgraded for better stability (also locked the engine instance as it's not thread safe)
updated readme
remove vpn detection from application configuration as it's now in a seperate plugin
defaulted webfront bind URl to all interfaces
readd the custom say name
added visibility percentage to AC
2018-09-04 12:40:29 -05:00
672d45df7c fix for issue #45 and #37 2018-09-02 22:09:25 -05:00
20d4ab27d3 add warn event
add alert to IW4ScriptCommands
2018-09-02 21:25:09 -05:00
e77ef69ee8 Added additional properties method to allow easier extension to client properties
updated VPN plugin to use WebClient
message is sent to client trying to execute commands before they are authenticated
fixed rare issue with ToAdmins failing
record bullet distance fraction for client kills (_customcallbacks)
change client level/permissions through webfront
ability to tempban through webfront
2018-09-02 16:59:27 -05:00
cc7628d058 fixed broken broadcast events
events don't get out of order when a invalid event line throws exception
handle the stats history update with no change throwing DBConcurrencyException
2018-08-31 22:35:51 -05:00
46bdc2ac33 moved event API stuff around
finally fixed threading issue (which actually had to do with IW4x log outputs being out of sync (not an issue with my code). What a lot of headache over something that wasn't my fault.
2018-08-30 20:53:00 -05:00
bbefd53db4 think I finished reworking the event system
added http log reading support for debugging remotely
started working on unit test framework
2018-08-28 16:32:59 -05:00
56cb8c50e7 reworked event management (again)
almost finished
2018-08-27 17:07:54 -05:00
0538d9f479 started update for readme
start update for version changes
hopefully fixed pesky stat bug
move vpn detection into script plugin
2018-08-26 19:20:47 -05:00
1343d4959e more support for javascript plugins 2018-08-23 16:16:30 -05:00
ac64d8d3c1 fixed unicode crap stuff in webhook
enable preview of tiered compiliation (faster startup)
ban events are sent to the API properly now
add vpn except id configuration
begin work on javascript plugin support
2018-08-22 20:25:34 -05:00
b5939bbdaf add more event types to discord webhook 2018-08-08 15:36:42 -05:00
a0fafe5797 cleaned up some namespace discrepancies
fixed the coloring for custom groups translation
add reserved slots
add webhook project to show notifications in discord
2018-08-07 13:43:09 -05:00
bbade07646 add localized level names
intellisense suggestion junk
2018-08-03 21:11:58 -05:00
3c0e101f14 appeal website is always show on kick
previously banned for recursion fixed
2018-08-03 19:06:47 -05:00
d0be08629d add page list to manager so we can inject pages into the layout view 2018-08-03 17:10:20 -05:00
9d00d5a16a removed event controller, and added status to api controller
get time passed returns weeks after 90 days
and months after 365
2018-08-02 20:52:35 -05:00
396e5c9215 confirmed working for linux
fixed the database access issue
2018-08-01 21:09:22 -05:00
4ec16d3aa2 increased max events for event api to 100
added GameInfo to EventInfo class
make sure score gets updated properly after authentication
2018-07-30 19:31:00 -05:00
f40bcce44f update to .NET Core 2.1
fix bower repo deprecation
2018-07-29 14:43:42 -05:00
6071ad8653 fix bug with AC failing to ban because of EF issue. 2018-07-05 21:04:34 -05:00
16d7ccd590 fix parsing of certain chat messages
print out the correct exception message when a server is not responding.
prevent log reader from reading before the servers have initialized
2018-07-04 21:09:42 -05:00
87541c4a5a more changes to the event management.
bots ip adjusted
2018-07-01 19:30:38 -05:00
af6361144e moved validate command into shared library.
reworked connection system to read from log file for join/quits and authenticate later with polling
2018-06-30 20:55:16 -05:00
454238a192 finer version numbers work correctly.
fix bug with level being reset.
add {{ADMINS}} to message tokens
modified offset threshold calculation
2018-06-26 20:17:24 -05:00
e7c7145da1 Show time passed since ban instead of "forever"
reworked event api to include all events (sans unknown)
2018-06-16 21:11:25 -05:00
5be6b75ccf [webfront] search by ip and name
[application] levels set properly with multiple GUIDs
[stats] require 3 hours of playtime for top stats recognition
[application] configurable rcon polling rate
2018-06-07 21:19:12 -05:00
e60f612f95 [application] added chat context to profile page
[iw4script] reworked balance to balance based on performance rating
[stats] log penalty context to database
2018-06-05 16:31:36 -05:00
ba023ceeb5 [application] added next map command and token 2018-06-02 21:21:01 -05:00
e3dba96d72 [sharedlibrary] add client meta
[application] add gravatar command
2018-06-01 23:48:10 -05:00
6d0f859a93 more updates to top stats page 2018-06-01 19:55:26 -05:00
696e2d12c9 change table design for rating history 2018-05-31 19:17:52 -05:00
bf68e5672f Add automated ban offense for anti-cheat
add EFClientStatHistory and EFClientAverageStatHistory for tracking change of stats over time
2018-05-30 20:50:20 -05:00
2204686b08 added top player stats
fix for some commands returning multiple matches found when target not required
2018-05-28 20:30:31 -05:00
1266 changed files with 267847 additions and 15982 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: raidmax

25
.gitignore vendored
View File

@ -220,7 +220,30 @@ Thumbs.db
DEPLOY
global.min.css
global.min.js
bootstrap-custom.css
bootstrap-custom.min.css
bootstrap-custom.css
**/Master/static
**/Master/dev_env
/WebfrontCore/wwwroot/**/dds
/WebfrontCore/wwwroot/images/radar/*
/DiscordWebhook/env
/DiscordWebhook/config.dev.json
/GameLogServer/env
launchSettings.json
/VpnDetectionPrivate.js
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
**/Master/env_master
/GameLogServer/log_env
**/*.css
/Master/master/persistence
/WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font
/Plugins/Tests/TestSourceFiles
/Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env
.idea/*
*.db
/Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal

View File

@ -1,75 +0,0 @@
using System;
using System.Collections.Generic;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.API
{
class EventApi : IEventApi
{
Queue<EventInfo> Events = new Queue<EventInfo>();
DateTime LastFlagEvent;
static string[] FlaggedMessageContains =
{
" wh ",
"hax",
"cheat",
" hack ",
"aim",
"wall",
"cheto",
"hak",
"bot"
};
int FlaggedMessageCount;
public Queue<EventInfo> GetEvents() => Events;
public void OnServerEvent(object sender, GameEvent E)
{
if (E.Type == GameEvent.EventType.Say && E.Origin.Level < Player.Permission.Trusted)
{
bool flaggedMessage = false;
foreach (string msg in FlaggedMessageContains)
flaggedMessage = flaggedMessage ? flaggedMessage : E.Data.ToLower().Contains(msg);
if (flaggedMessage)
FlaggedMessageCount++;
if (FlaggedMessageCount > 3)
{
if (Events.Count > 20)
Events.Dequeue();
FlaggedMessageCount = 0;
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_REPORT"]).Wait(5000);
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
"Chat indicates there may be a cheater",
"Alert",
E.Owner.Hostname, ""));
}
if ((DateTime.UtcNow - LastFlagEvent).Minutes >= 3)
{
FlaggedMessageCount = 0;
LastFlagEvent = DateTime.Now;
}
}
if (E.Type == GameEvent.EventType.Report)
{
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
$"**{E.Origin.Name}** has reported **{E.Target.Name}** for: {E.Data.Trim()}",
E.Target.Name, E.Origin.Name, ""));
}
}
}
}

View File

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using RestEase;
namespace IW4MAdmin.Application.API.GameLogServer
{
[Header("User-Agent", "IW4MAdmin-RestEase")]
public interface IGameLogServer
{
[Get("log/{path}/{key}")]
Task<LogInfo> Log([Path] string path, [Path] string key);
}
}

View File

@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.API.GameLogServer
{
public class LogInfo
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("length")]
public int Length { get; set; }
[JsonProperty("data")]
public string Data { get; set; }
[JsonProperty("next_key")]
public string NextKey { get; set; }
}
}

View File

@ -1,20 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Application.API.Master
{
/// <summary>
/// Defines the structure of the IW4MAdmin instance for the master API
/// </summary>
public class ApiInstance
{
/// <summary>
/// Unique ID of the instance
/// </summary>
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// Indicates how long the instance has been running
/// </summary>
[JsonProperty("uptime")]
public int Uptime { get; set; }
/// <summary>
/// Specifies the version of the instance
/// </summary>
[JsonProperty("version")]
public float Version { get; set; }
[JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber Version { get; set; }
/// <summary>
/// List of servers the instance is monitoring
/// </summary>
[JsonProperty("servers")]
public List<ApiServer> Servers { get; set; }
/// <summary>
/// Url IW4MAdmin is listening on
/// </summary>
[JsonProperty("webfront_url")]
public string WebfrontUrl { get; set; }
}
}

View File

@ -8,9 +8,13 @@ namespace IW4MAdmin.Application.API.Master
public class ApiServer
{
[JsonProperty("id")]
public int Id { get; set; }
public long Id { get; set; }
[JsonProperty("ip")]
public string IPAddress { get; set; }
[JsonProperty("port")]
public short Port { get; set; }
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("gametype")]
public string Gametype { get; set; }
[JsonProperty("map")]

View File

@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RestEase;
using SharedLibraryCore;
namespace IW4MAdmin.Application.API.Master
{
public class HeartbeatState
{
public bool Connected { get; set; }
}
public class Heartbeat
{
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
{
var api = Endpoint.Get();
if (firstHeartbeat)
{
var token = await api.Authenticate(new AuthenticationId()
{
Id = mgr.GetApplicationSettings().Configuration().Id
});
api.AuthorizationToken = $"Bearer {token.AccessToken}";
}
var instance = new ApiInstance()
{
Id = mgr.GetApplicationSettings().Configuration().Id,
Uptime = (int)(DateTime.UtcNow - mgr.StartTime).TotalSeconds,
Version = (float)Program.Version,
Servers = mgr.Servers.Select(s =>
new ApiServer()
{
ClientNum = s.ClientNum,
Game = s.GameName.ToString(),
Gametype = s.Gametype,
Hostname = s.Hostname,
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.GetHashCode(),
Port = (short)s.GetPort()
}).ToList()
};
if (firstHeartbeat)
{
var message = await api.AddInstance(instance);
}
else
{
var message = await api.UpdateInstance(instance.Id, instance);
}
}
}
}

View File

@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using IW4MAdmin.Application.Plugin;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Application.API.Master
{
@ -22,9 +23,12 @@ namespace IW4MAdmin.Application.API.Master
public class VersionInfo
{
[JsonProperty("current-version-stable")]
public float CurrentVersionStable { get; set; }
[JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber CurrentVersionStable { get; set; }
[JsonProperty("current-version-prerelease")]
public float CurrentVersionPrerelease { get; set; }
[JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber CurrentVersionPrerelease { get; set; }
}
public class ResultMessage
@ -33,16 +37,16 @@ namespace IW4MAdmin.Application.API.Master
public string Message { get; set; }
}
public class Endpoint
public class PluginSubscriptionContent
{
#if !DEBUG
private static IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
#else
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
#endif
public static IMasterApi Get() => api;
public string Content { get; set; }
public PluginType Type { get; set; }
}
/// <summary>
/// Defines the capabilities of the master API
/// </summary>
[Header("User-Agent", "IW4MAdmin-RestEase")]
public interface IMasterApi
{
@ -53,18 +57,23 @@ namespace IW4MAdmin.Application.API.Master
Task<TokenId> Authenticate([Body] AuthenticationId Id);
[Post("instance/")]
Task<ResultMessage> AddInstance([Body] ApiInstance instance);
[AllowAnyStatusCode]
Task<Response<ResultMessage>> AddInstance([Body] ApiInstance instance);
[Put("instance/{id}")]
Task<ResultMessage> UpdateInstance([Path] string id, [Body] ApiInstance instance);
[AllowAnyStatusCode]
Task<Response<ResultMessage>> UpdateInstance([Path] string id, [Body] ApiInstance instance);
[Get("version")]
Task<VersionInfo> GetVersion();
[Get("version/{apiVersion}")]
Task<VersionInfo> GetVersion([Path] int apiVersion);
[Get("localization")]
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();
[Get("localization/{languageTag}")]
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag);
[Get("plugin_subscriptions")]
Task<IEnumerable<PluginSubscriptionContent>> GetPluginSubscription([Query("instance_id")] Guid instanceId, [Query("subscription_id")] string subscription_id);
}
}

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

View File

@ -2,15 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.1.0</Version>
<Version>2020.0.0.0</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Product>IW4MAdmin</Product>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description>
<Copyright>2018</Copyright>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
<Copyright>2020</Copyright>
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
@ -20,74 +20,67 @@
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
<PackageReference Include="Jint" Version="3.0.0-beta-2047" />
<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.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
<ErrorReport>none</ErrorReport>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Integrations\Cod\Integrations.Cod.csproj" />
<ProjectReference Include="..\Integrations\Source\Integrations.Source.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>true</Private>
</ProjectReference>
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj">
<Private>true</Private>
<CopyLocalSatelliteAssemblies>true</CopyLocalSatelliteAssemblies>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\IW4MAdmin.en-US.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>IW4MAdmin.en-US.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\IW4MAdmin.en-US.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>IW4MAdmin.en-US.Designer.cs</LastGenOutput>
</EmbeddedResource>
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="DefaultSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.en-US.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.es-EC.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<None Update="Resources\GeoLite2-Country.mmdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.pt-BR.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.ru-RU.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %25(CurrentAssembly.Version)" />
</Target>
<Target Name="PostPublish" AfterTargets="Publish">
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
</Target>
</Project>

View File

@ -0,0 +1,859 @@
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RConParsers;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Context;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore.Events;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Formatting;
using SharedLibraryCore.Interfaces.Events;
using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
[Obsolete] public ObsoleteLogger Logger => _serviceProvider.GetRequiredService<ObsoleteLogger>();
public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; }
public DateTime StartTime { get; private set; }
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
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 => _isRunningTokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
public event EventHandler<GameEvent> OnGameEventExecuted;
private readonly List<IManagerCommand> _commands;
private readonly ILogger _logger;
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 CancellationTokenSource _isRunningTokenSource;
private CancellationTokenSource _eventHandlerTokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly ICoreEventHandler _coreEventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
private readonly IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig;
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
{
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) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_isRunningTokenSource = new CancellationTokenSource();
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
_coreEventHandler = coreEventHandler;
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
_serviceProvider = serviceProvider;
_changeHistoryService = changeHistoryService;
_appConfig = appConfig;
Plugins = plugins;
InteractionRegistration = interactionRegistration;
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
}
public IEnumerable<IPlugin> Plugins { get; }
public IInteractionRegistration InteractionRegistration { get; }
public async Task ExecuteEvent(GameEvent newEvent)
{
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
// the event has failed already
if (newEvent.Failed)
{
goto skip;
}
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database
await _changeHistoryService.Add(newEvent);
}
catch (TaskCanceledException)
{
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
}
catch (OperationCanceledException)
{
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
}
// this happens if a plugin requires login
catch (AuthorizationException ex)
{
newEvent.FailReason = EventFailReason.Permission;
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
}
catch (NetworkException ex)
{
newEvent.FailReason = EventFailReason.Exception;
using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, ex.Message);
}
}
catch (ServerException ex)
{
newEvent.FailReason = EventFailReason.Exception;
using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, ex.Message);
}
}
catch (Exception ex)
{
newEvent.FailReason = EventFailReason.Exception;
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, "Unexpected exception");
}
}
skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
ev.WaitAsync(Utilities.DefaultCommandTimeout, CancellationToken)));
newEvent.Output.AddRange(correlatedEvents.SelectMany(ev => ev.Output));
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.Count(gameEvent =>
newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
newEvent.CorrelationId is null)
{
ProcessingEvents.Remove(newEvent.IncrementalId, out _);
}
// tell anyone waiting for the output that we're done
newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent);
}
public IList<Server> GetServers()
{
return Servers;
}
public IList<IManagerCommand> GetCommands()
{
return _commands;
}
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
private Task UpdateServerStates()
{
var index = 0;
return Task.WhenAll(_servers.Select(server =>
{
var thisIndex = index;
Interlocked.Increment(ref index);
return ProcessUpdateHandler(server, thisIndex);
}));
}
private async Task ProcessUpdateHandler(Server server, int index)
{
const int delayScalar = 50; // Task.Delay is inconsistent enough there's no reason to try to prevent collisions
var timeout = TimeSpan.FromMinutes(2);
while (!_isRunningTokenSource.IsCancellationRequested)
{
try
{
var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index);
await Task.Delay(delayFactor, _isRunningTokenSource.Token);
using var timeoutTokenSource = new CancellationTokenSource();
timeoutTokenSource.CancelAfter(timeout);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token,
_isRunningTokenSource.Token);
await server.ProcessUpdatesAsync(linkedTokenSource.Token);
await Task.Delay(Math.Max(1000, _appConfig.RConPollRate - delayFactor),
_isRunningTokenSource.Token);
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.Id))
{
_logger.LogError(ex, "Failed to update status");
}
}
finally
{
server.IsInitialized = true;
}
}
// run the final updates to clean up server
await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
}
public async Task Init()
{
IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP();
#region DATABASE
_logger.LogInformation("Beginning database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
#region EVENTS
IGameServerEventSubscriptions.ServerValueRequested += OnServerValueRequested;
IGameServerEventSubscriptions.ServerValueSetRequested += OnServerValueSetRequested;
await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
# endregion
#region PLUGINS
foreach (var plugin in Plugins)
{
try
{
if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
}
catch (Exception ex)
{
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
_logger.LogError(ex, "Could not properly load plugin {plugin}", scriptPlugin.Name);
}
};
}
else
{
await plugin.OnLoadAsync(this);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
}
}
#endregion
#region CONFIG
// copy over default config if it doesn't exist
if (!_appConfig.Servers?.Any() ?? true)
{
var defaultHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultHandler.BuildAsync();
var defaultConfig = defaultHandler.Configuration();
_appConfig.AutoMessages = defaultConfig.AutoMessages;
_appConfig.GlobalRules = defaultConfig.GlobalRules;
_appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
//if (newConfig.Servers == null)
{
ConfigHandler.Set(_appConfig);
_appConfig.Servers = new ServerConfiguration[1];
do
{
var serverConfig = new ServerConfiguration();
foreach (var parser in AdditionalRConParsers)
{
serverConfig.AddRConParser(parser);
}
foreach (var parser in AdditionalEventParsers)
{
serverConfig.AddEventParser(parser);
}
_appConfig.Servers = _appConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
} while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
await ConfigHandler.Save();
}
}
else
{
if (string.IsNullOrEmpty(_appConfig.Id))
{
_appConfig.Id = Guid.NewGuid().ToString();
}
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
}
#pragma warning disable 618
if (_appConfig.Maps != null)
{
_appConfig.Maps = null;
}
if (_appConfig.QuickMessages != null)
{
_appConfig.QuickMessages = null;
}
#pragma warning restore 618
var validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(_appConfig);
if (!validationResult.IsValid)
{
throw new ConfigurationException("Could not validate configuration")
{
Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(),
ConfigurationFileName = ConfigHandler.FileName
};
}
foreach (var serverConfig in _appConfig.Servers)
{
ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{
foreach (var parser in AdditionalRConParsers)
{
serverConfig.AddRConParser(parser);
}
foreach (var parser in AdditionalEventParsers)
{
serverConfig.AddEventParser(parser);
}
serverConfig.ModifyParsers();
}
}
await ConfigHandler.Save();
}
if (_appConfig.Servers.Length == 0)
{
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
foreach (var parser in AdditionalRConParsers)
{
if (!parser.Configuration.ColorCodeMapping.ContainsKey(ColorCodes.Accent.ToString()))
{
parser.Configuration.ColorCodeMapping.Add(ColorCodes.Accent.ToString(),
parser.Configuration.ColorCodeMapping.TryGetValue(_appConfig.IngameAccentColorKey, out var colorCode)
? colorCode
: "");
}
}
#endregion
#region COMMANDS
if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
{
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
List<IManagerCommand> commandsToAddToConfig = new List<IManagerCommand>();
var cmdConfig = _commandConfiguration.Configuration();
if (cmdConfig == null)
{
cmdConfig = new CommandConfiguration();
commandsToAddToConfig.AddRange(_commands);
}
else
{
var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.CommandConfigNameForType()));
commandsToAddToConfig.AddRange(unsavedCommands);
}
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it
cmdConfig.CommandPrefix = _appConfig?.CommandPrefix ?? "!";
cmdConfig.BroadcastCommandPrefix = _appConfig?.BroadcastCommandPrefix ?? "@";
foreach (var cmd in commandsToAddToConfig)
{
if (cmdConfig.Commands.ContainsKey(cmd.CommandConfigNameForType()))
{
continue;
}
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties
{
Name = cmd.Name,
Alias = cmd.Alias,
MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation,
SupportedGames = cmd.SupportedGames
});
}
_commandConfiguration.Set(cmdConfig);
await _commandConfiguration.Save();
#endregion
_metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
{
foreach (var parser in AdditionalEventParsers)
{
parser.RegisterCustomEvent(customEvent.Item1, customEvent.Item2, customEvent.Item3);
}
}
#endregion
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers();
IsInitialized = true;
}
private async Task InitializeServers()
{
var config = ConfigHandler.Configuration();
int successServers = 0;
Exception lastException = null;
async Task Init(ServerConfiguration Conf)
{
try
{
// todo: this might not always be an IW4MServer
var serverInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
using (LogContext.PushProperty("Server", serverInstance!.ToString()))
{
_logger.LogInformation("Beginning server communication initialization");
await serverInstance.Initialize();
_servers.Add(serverInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(serverInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", serverInstance.Hostname);
}
QueueEvent(new MonitorStartEvent
{
Server = serverInstance,
Source = this
});
successServers++;
}
catch (ServerException e)
{
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
using (LogContext.PushProperty("Server", $"{Conf.IPAddress}:{Conf.Port}"))
{
_logger.LogError(e, "Unexpected exception occurred during initialization");
}
lastException = e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
if (successServers == 0)
{
throw lastException;
}
if (successServers != config.Servers.Length)
{
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}
}
}
public async Task Start()
{
_eventHandlerTokenSource = new CancellationTokenSource();
var eventHandlerThread = new Thread(() =>
{
_coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
})
{
Name = nameof(CoreEventHandler)
};
eventHandlerThread.Start();
await UpdateServerStates();
_eventHandlerTokenSource.Cancel();
eventHandlerThread.Join();
}
public async Task Stop()
{
foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
{
try
{
await plugin.OnUnloadAsync().WithTimeout(Utilities.DefaultCommandTimeout);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
}
}
_isRunningTokenSource.Cancel();
IsRunning = false;
}
public async Task Restart()
{
IsRestartRequested = true;
await Stop();
using var subscriptionTimeoutToken = new CancellationTokenSource();
subscriptionTimeoutToken.CancelAfter(Utilities.DefaultCommandTimeout);
await IManagementEventSubscriptions.InvokeUnloadAsync(this, subscriptionTimeoutToken.Token);
IGameEventSubscriptions.ClearEventInvocations();
IGameServerEventSubscriptions.ClearEventInvocations();
IManagementEventSubscriptions.ClearEventInvocations();
_isRunningTokenSource.Dispose();
_isRunningTokenSource = new CancellationTokenSource();
_eventHandlerTokenSource.Dispose();
_eventHandlerTokenSource = new CancellationTokenSource();
}
[Obsolete]
public ObsoleteLogger GetLogger(long serverId)
{
return _serviceProvider.GetRequiredService<ObsoleteLogger>();
}
public IList<MessageToken> GetMessageTokens()
{
return MessageTokens;
}
public IList<EFClient> GetActiveClients()
{
// we're adding another to list here so we don't get a collection modified exception..
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
}
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client;
public ClientService GetClientService()
{
return ClientSvc;
}
public PenaltyService GetPenaltyService()
{
return PenaltySvc;
}
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings()
{
return ConfigHandler;
}
public void AddEvent(GameEvent gameEvent)
{
_coreEventHandler.QueueEvent(this, gameEvent);
}
public void QueueEvent(CoreEvent coreEvent)
{
_coreEventHandler.QueueEvent(this, coreEvent);
}
public IPageList GetPageList()
{
return PageList;
}
public IRConParser GenerateDynamicRConParser(string name)
{
return new DynamicRConParser(_serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), _parserRegexFactory)
{
Name = name
};
}
public IEventParser GenerateDynamicEventParser(string name)
{
return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration())
{
Name = name
};
}
public async Task<IList<T>> ExecuteSharedDatabaseOperation<T>(string operationName)
{
var result = await _operationLookup[operationName];
return (IList<T>)result;
}
public void RegisterSharedDatabaseOperation(Task<IList> operation, string operationName)
{
_operationLookup.Add(operationName, operation);
}
public void AddAdditionalCommand(IManagerCommand command)
{
lock (_commands)
{
if (_commands.Any(cmd => cmd.Name == command.Name || cmd.Alias == command.Alias))
{
throw new InvalidOperationException(
$"Duplicate command name or alias ({command.Name}, {command.Alias})");
}
_commands.Add(command);
}
}
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
private async Task OnServerValueRequested(ServerValueRequestEvent requestEvent, CancellationToken token)
{
if (requestEvent.Server is not IW4MServer server)
{
return;
}
Dvar<string> serverValue = null;
try
{
if (requestEvent.DelayMs.HasValue)
{
await Task.Delay(requestEvent.DelayMs.Value, token);
}
var waitToken = token;
using var timeoutTokenSource = new CancellationTokenSource();
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
if (requestEvent.TimeoutMs is not null)
{
timeoutTokenSource.CancelAfter(requestEvent.TimeoutMs.Value);
waitToken = linkedTokenSource.Token;
}
serverValue =
await server.GetDvarAsync(requestEvent.ValueName, requestEvent.FallbackValue, waitToken);
}
catch
{
// ignored
}
finally
{
QueueEvent(new ServerValueReceiveEvent
{
Server = server,
Source = server,
Response = serverValue ?? new Dvar<string> { Name = requestEvent.ValueName },
Success = serverValue is not null
});
}
}
private async Task OnServerValueSetRequested(ServerValueSetRequestEvent requestEvent, CancellationToken token)
{
if (requestEvent.Server is not IW4MServer server)
{
return;
}
var completed = false;
try
{
if (requestEvent.DelayMs.HasValue)
{
await Task.Delay(requestEvent.DelayMs.Value, token);
}
if (requestEvent.TimeoutMs is not null)
{
using var timeoutTokenSource = new CancellationTokenSource(requestEvent.TimeoutMs.Value);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
token = linkedTokenSource.Token;
}
await server.SetDvarAsync(requestEvent.ValueName, requestEvent.Value, token);
completed = true;
}
catch
{
// ignored
}
finally
{
QueueEvent(new ServerValueSetCompleteEvent
{
Server = server,
Source = server,
Success = completed,
Value = requestEvent.Value,
ValueName = requestEvent.ValueName
});
}
}
private async Task OnClientPersistentIdReceived(ClientPersistentIdReceiveEvent receiveEvent, CancellationToken token)
{
var parts = receiveEvent.PersistentId.Split(",");
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
int.TryParse(parts[1], out var low))
{
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
var penalties = await PenaltySvc
.GetActivePenaltiesByIdentifier(null, guid, receiveEvent.Client.GameName);
var banPenalty =
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (banPenalty is not null && receiveEvent.Client.Level != Data.Models.Client.EFClient.Permission.Banned)
{
_logger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
receiveEvent.Client, guid);
receiveEvent.Client.Ban(_translationLookup["SERVER_BAN_EVADE"].FormatExt(guid),
receiveEvent.Client.CurrentServer.AsConsoleClient(), true);
}
}
}
}
}

View File

@ -0,0 +1,12 @@
param (
[string]$OutputDir = $(throw "-OutputDir is required.")
)
$localizations = @("en-US", "ru-RU", "es-EC", "pt-BR", "de-DE")
foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
$response = Invoke-WebRequest $url -UseBasicParsing
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
}

View File

@ -2,10 +2,11 @@ set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set OutDir=%4
set Version=%5
echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
copy "%SolutionDir%SharedLibaryCore\%OutDir%*.deps.json" "%TargetDir%"
copy "%SolutionDir%SharedLibraryCore\%OutDir%*.deps.json" "%TargetDir%"
if not exist "%TargetDir%Plugins" (
echo "Making plugin dir"
@ -13,8 +14,4 @@ if not exist "%TargetDir%Plugins" (
)
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
echo Copying plugins for publish
del %SolutionDir%BUILD\Plugins\Tests.dll
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
del "%TargetDir%Plugins\SQLite*"

View File

@ -1,59 +1,67 @@
set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set PublishDir=%1
set SourceDir=%2
SET COPYCMD=/Y
echo Deleting extra language files
echo deleting extra runtime files
if exist "%PublishDir%\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-arm'
if exist "%PublishDir%\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-arm64'
if exist "%PublishDir%\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-armel'
if exist "%PublishDir%\runtimes\osx" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\osx'
if exist "%PublishDir%\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\osx-x64'
if exist "%PublishDir%\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\win-arm'
if exist "%PublishDir%\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\win-arm64'
if exist "%PublishDir%\runtimes\alpine-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\alpine-x64'
if exist "%PublishDir%\runtimes\linux-musl-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-musl-x64'
if exist "%SolutionDir%Publish\Windows\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\en-US'
if exist "%SolutionDir%Publish\Windows\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\de'
if exist "%SolutionDir%Publish\Windows\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\es'
if exist "%SolutionDir%Publish\Windows\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\fr'
if exist "%SolutionDir%Publish\Windows\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\it'
if exist "%SolutionDir%Publish\Windows\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ja'
if exist "%SolutionDir%Publish\Windows\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ko'
if exist "%SolutionDir%Publish\Windows\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ru'
if exist "%SolutionDir%Publish\Windows\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hans'
if exist "%SolutionDir%Publish\Windows\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hant'
echo deleting misc files
if exist "%PublishDir%\web.config" del "%PublishDir%\web.config"
if exist "%PublishDir%\libman.json" del "%PublishDir%\libman.json"
del "%PublishDir%\*.exe"
del "%PublishDir%\*.pdb"
if exist "%SolutionDir%Publish\WindowsPrerelease\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\en-US'
if exist "%SolutionDir%Publish\WindowsPrerelease\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\de'
if exist "%SolutionDir%Publish\WindowsPrerelease\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\es'
if exist "%SolutionDir%Publish\WindowsPrerelease\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\fr'
if exist "%SolutionDir%Publish\WindowsPrerelease\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\it'
if exist "%SolutionDir%Publish\WindowsPrerelease\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ja'
if exist "%SolutionDir%Publish\WindowsPrerelease\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ko'
if exist "%SolutionDir%Publish\WindowsPrerelease\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ru'
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hans'
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hant'
echo setting up default folders
if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration"
move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\"
if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
del "%PublishDir%\Microsoft.CodeAnalysis*.dll" /F /Q
move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
move "%PublishDir%\de" "%PublishDir%\Lib\de"
move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
move "%PublishDir%\es" "%PublishDir%\Lib\es"
rmdir /Q /S "%PublishDir%\cs"
rmdir /Q /S "%PublishDir%\fr"
rmdir /Q /S "%PublishDir%\it"
rmdir /Q /S "%PublishDir%\ja"
rmdir /Q /S "%PublishDir%\ko"
rmdir /Q /S "%PublishDir%\pl"
rmdir /Q /S "%PublishDir%\pt-BR"
rmdir /Q /S "%PublishDir%\tr"
rmdir /Q /S "%PublishDir%\zh-Hans"
rmdir /Q /S "%PublishDir%\zh-Hant"
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo Deleting extra runtime files
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm'
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm64'
if exist "%SolutionDir%Publish\Windows\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-armel'
echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh"
if exist "%SolutionDir%Publish\Windows\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\osx'
if exist "%SolutionDir%Publish\Windows\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\osx-x64'
echo copying update scripts
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.ps1" "%PublishDir%\UpdateIW4MAdmin.ps1"
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.sh" "%PublishDir%\UpdateIW4MAdmin.sh"
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm'
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm64'
echo moving front-end library dependencies
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\"
if exist "%PublishDir%\wwwroot\lib" rd /s /q "%PublishDir%\wwwroot\lib"
if not exist "%PublishDir%\wwwroot\css" mkdir "%PublishDir%\wwwroot\css"
move "WebfrontCore\wwwroot\css\global.min.css" "%PublishDir%\wwwroot\css\global.min.css"
if not exist "%PublishDir%\wwwroot\js" mkdir "%PublishDir%\wwwroot\js"
move "%SourceDir%\WebfrontCore\wwwroot\js\global.min.js" "%PublishDir%\wwwroot\js\global.min.js"
if not exist "%PublishDir%\wwwroot\images" mkdir "%PublishDir%\wwwroot\images"
xcopy "%SourceDir%\WebfrontCore\wwwroot\images" "%PublishDir%\wwwroot\images" /E /H /C /I
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64'
echo Deleting misc files
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
del "%SolutionDir%Publish\Windows\*pdb"
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
echo making start script
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
echo setting permissions...
cacls "%PublishDir%" /t /e /p Everyone:F

View File

@ -1,3 +1,6 @@
set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
echo D | xcopy "%SolutionDir%Plugins\ScriptPlugins\*.js" "%TargetDir%Plugins" /y
powershell -File "%ProjectDir%BuildScripts\DownloadTranslations.ps1" %TargetDir%

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

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class AddClientTagCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public AddClientTagCommand(ILogger<AddClientTagCommand> commandLogger, CommandConfiguration config,
ITranslationLookup layout, IMetaServiceV2 metaService) :
base(config, layout)
{
Name = "addclienttag";
Description = layout["COMMANDS_ADD_CLIENT_TAG_DESC"];
Alias = "act";
Permission = EFClient.Permission.Owner;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
logger = commandLogger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var existingTags = await _metaService.GetPersistentMetaValue<List<TagMeta>>(EFMeta.ClientTagNameV2) ??
new List<TagMeta>();
var tagName = gameEvent.Data.Trim();
if (existingTags.Any(tag => tag.TagName == tagName))
{
logger.LogWarning("Tag with name {TagName} already exists", tagName);
return;
}
existingTags.Add(new TagMeta
{
Id = (existingTags.LastOrDefault()?.TagId ?? 0) + 1,
Value = tagName
});
await _metaService.SetPersistentMetaValue(EFMeta.ClientTagNameV2, existingTags,
gameEvent.Owner.Manager.CancellationToken);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_TAG_SUCCESS"].FormatExt(gameEvent.Data));
}
}
}

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class ListClientTags : Command
{
private readonly IMetaServiceV2 _metaService;
public ListClientTags(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(
config, layout)
{
Name = "listclienttags";
Description = layout["COMMANDS_LIST_CLIENT_TAGS_DESC"];
Alias = "lct";
Permission = EFClient.Permission.Owner;
RequiresTarget = false;
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var tags = await _metaService.GetPersistentMetaValue<List<TagMeta>>(EFMeta.ClientTagNameV2);
if (tags is not null)
{
await gameEvent.Origin.TellAsync(tags.Select(tag => tag.TagName),
gameEvent.Owner.Manager.CancellationToken);
}
}
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class RemoveClientTag : Command
{
private readonly IMetaServiceV2 _metaService;
public RemoveClientTag(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(
config, layout)
{
Name = "removeclienttag";
Description = layout["COMMANDS_REMOVE_CLIENT_TAG_DESC"];
Alias = "rct";
Permission = EFClient.Permission.Owner;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var existingMeta = await _metaService.GetPersistentMetaValue<List<TagMeta>>(EFMeta.ClientTagNameV2,
gameEvent.Owner.Manager.CancellationToken);
existingMeta = existingMeta.Where(meta => meta.TagName != gameEvent.Data.Trim()).ToList();
await _metaService.SetPersistentMetaValue(EFMeta.ClientTagNameV2, existingMeta,
gameEvent.Owner.Manager.CancellationToken);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REMOVE_CLIENT_TAG_SUCCESS"].FormatExt(gameEvent.Data));
}
}
}

View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class SetClientTagCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public SetClientTagCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) :
base(config, layout)
{
Name = "setclienttag";
Description = layout["COMMANDS_SET_CLIENT_TAG_DESC"];
Alias = "sct";
Permission = EFClient.Permission.Owner;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var token = gameEvent.Owner.Manager.CancellationToken;
var availableTags = await _metaService.GetPersistentMetaValue<List<LookupValue<string>>>(EFMeta.ClientTagNameV2, token);
var matchingTag = availableTags.FirstOrDefault(tag => tag.Value == gameEvent.Data.Trim());
if (matchingTag == null)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SET_CLIENT_TAG_FAIL"].FormatExt(gameEvent.Data));
return;
}
gameEvent.Target.Tag = matchingTag.Value;
await _metaService.SetPersistentMetaForLookupKey(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, matchingTag.Id,
gameEvent.Target.ClientId, token);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SET_CLIENT_TAG_SUCCESS"].FormatExt(matchingTag.Value));
}
}
}

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags;
public class TagMeta : ILookupValue<string>
{
[JsonIgnore] public int TagId => Id;
[JsonIgnore] public string TagName => Value;
public int Id { get; set; }
public string Value { get; set; }
}

View File

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands.ClientTags
{
public class UnsetClientTagCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public UnsetClientTagCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) :
base(config, layout)
{
Name = "unsetclienttag";
Description = layout["COMMANDS_UNSET_CLIENT_TAG_DESC"];
Alias = "uct";
Permission = EFClient.Permission.Owner;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGUMENT_TAG"],
Required = true
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Target.Tag = null;
await _metaService.RemovePersistentMeta(EFMeta.ClientTagV2, gameEvent.Target.ClientId,
gameEvent.Owner.Manager.CancellationToken);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_UNSET_CLIENT_TAG_SUCCESS"]);
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Finds player by name
/// </summary>
public class FindPlayerCommand : Command
{
public FindPlayerCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "find";
Description = _translationLookup["COMMANDS_FIND_DESC"];
Alias = "f";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data.Length < 3)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_MIN"]);
return;
}
var players = await gameEvent.Owner.Manager.GetClientService().FindClientsByIdentifier(gameEvent.Data);
if (!players.Any())
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_EMPTY"]);
return;
}
foreach (var client in players)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_FORMAT_V2"].FormatExt(client.Name,
client.ClientId, Utilities.ConvertLevelToColor((EFClient.Permission) client.LevelInt, client.Level),
client.IPAddress, (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture()));
}
}
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints help information
/// </summary>
public class HelpCommand : Command
{
public HelpCommand(CommandConfiguration config, ITranslationLookup translationLookup) :
base(config, translationLookup)
{
Name = "help";
Description = translationLookup["COMMANDS_HELP_DESC"];
Alias = "h";
Permission = EFClient.Permission.User;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_COMMANDS"],
Required = false
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var searchTerm = gameEvent.Data.Trim();
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
command.SupportedGames == null || !command.SupportedGames.Any() ||
command.SupportedGames.Contains(gameEvent.Owner.GameName))
.Where(command => gameEvent.Origin.Level >= command.Permission);
if (searchTerm.Length > 2)
{
var matchingCommand = availableCommands.FirstOrDefault(command =>
command.Name.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase) ||
command.Alias.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase));
if (matchingCommand != null)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_SEARCH_RESULT"]
.FormatExt(matchingCommand.Name, matchingCommand.Alias));
gameEvent.Origin.Tell(matchingCommand.Syntax);
}
else
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_NOTFOUND"]);
}
}
else
{
var commandStrings = availableCommands.Select((command, index) =>
new
{
response = $" {_translationLookup["COMMANDS_HELP_LIST_FORMAT"].FormatExt(command.Name)} ",
index
});
var helpResponse = new StringBuilder();
var messageList = new List<string>();
foreach (var item in commandStrings)
{
helpResponse.Append(item.response);
if (item.index == 0 || item.index % 4 != 0)
{
continue;
}
messageList.Add(helpResponse.ToString());
helpResponse = new StringBuilder();
}
messageList.Add(helpResponse.ToString());
await gameEvent.Origin.TellAsync(messageList);
}
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists all unmasked admins
/// </summary>
public class ListAdminsCommand : Command
{
public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "admins";
Description = _translationLookup["COMMANDS_ADMINS_DESC"];
Alias = "a";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public static string OnlineAdmins(Server server, ITranslationLookup lookup)
{
var onlineAdmins = server.GetClientsAsList()
.Where(p => p.Level > EFClient.Permission.Flagged)
.Where(p => !p.Masked)
.Select(p =>
$"[(Color::Yellow){Utilities.ConvertLevelToColor(p.Level, p.ClientPermission.Name)}(Color::White)] {p.Name}")
.ToList();
return onlineAdmins.Any() ? string.Join(Environment.NewLine, onlineAdmins) : lookup["COMMANDS_ADMINS_NONE"];
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
foreach (var line in OnlineAdmins(gameEvent.Owner, _translationLookup).Split(Environment.NewLine))
{
var _ = gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)
? gameEvent.Owner.Broadcast(line)
: gameEvent.Origin.Tell(line);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists alises of specified client
/// </summary>
public class ListAliasesCommand : Command
{
public ListAliasesCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "alias";
Description = _translationLookup["COMMANDS_ALIAS_DESC"];
Alias = "known";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true,
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var message = new StringBuilder();
var names = new List<string>(gameEvent.Target.AliasLink.Children.Select(a => a.Name));
var ips = new List<string>(gameEvent.Target.AliasLink.Children.Select(a => a.IPAddress.ConvertIPtoString())
.Distinct());
gameEvent.Origin.Tell($"[(Color::Accent){gameEvent.Target}(Color::White)]");
message.Append($"{_translationLookup["COMMANDS_ALIAS_ALIASES"]}: ");
message.Append(string.Join(" | ", names));
gameEvent.Origin.Tell(message.ToString());
message.Clear();
message.Append($"{_translationLookup["COMMANDS_ALIAS_IPS"]}: ");
message.Append(string.Join(" | ", ips));
gameEvent.Origin.Tell(message.ToString());
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,37 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// List online clients
/// </summary>
public class ListClientsCommand : Command
{
public ListClientsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "list";
Description = _translationLookup["COMMANDS_LIST_DESC"];
Alias = "l";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var clientList = gameEvent.Owner.GetClientsAsList()
.Select(client =>
$"[(Color::Accent){client.ClientPermission.Name}(Color::White){(string.IsNullOrEmpty(client.Tag) ? "" : $" {client.Tag}")}(Color::White)][(Color::Yellow)#{client.ClientNumber}(Color::White)] {client.Name}")
.ToArray();
gameEvent.Origin.TellAsync(clientList, gameEvent.Owner.Manager.CancellationToken);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists the loaded plugins
/// </summary>
public class ListPluginsCommand : Command
{
private readonly IEnumerable<IPlugin> _plugins;
public ListPluginsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IEnumerable<IPlugin> plugins) : base(config, translationLookup)
{
Name = "plugins";
Description = _translationLookup["COMMANDS_PLUGINS_DESC"];
Alias = "p";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
_plugins = plugins;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_PLUGINS_LOADED"]);
foreach (var plugin in _plugins.Where(plugin => !plugin.IsParser))
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_LIST_PLUGINS_FORMAT"]
.FormatExt(plugin.Name, plugin.Version, plugin.Author));
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// List all reports on the server
/// </summary>
public class ListReportsCommand : Command
{
public ListReportsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "reports";
Description = _translationLookup["COMMANDS_REPORTS_DESC"];
Alias = "reps";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_CLEAR"],
Required = false
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data != null && gameEvent.Data.ToLower().Contains(_translationLookup["COMMANDS_ARGS_CLEAR"]))
{
gameEvent.Owner.Reports = new List<Report>();
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_CLEAR_SUCCESS"]);
return Task.CompletedTask;
}
if (gameEvent.Owner.Reports.Count < 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_NONE"]);
return Task.CompletedTask;
}
foreach (var report in gameEvent.Owner.Reports)
{
gameEvent.Origin.Tell(
$"(Color::Accent){report.Origin.Name}(Color::White) -> (Color::Red){report.Target.Name}(Color::White): {report.Reason}");
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,116 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Data.Models.Client;
using IW4MAdmin.Application.Extensions;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class MapAndGameTypeCommand : Command
{
private const string ArgumentRegexPattern = "(?:\"([^\"]+)\"|([^\\s]+)) (?:\"([^\"]+)\"|([^\\s]+))";
private readonly ILogger _logger;
private readonly DefaultSettings _defaultSettings;
public MapAndGameTypeCommand(ILogger<MapAndGameTypeCommand> logger, CommandConfiguration config,
DefaultSettings defaultSettings, ITranslationLookup layout) : base(config, layout)
{
Name = "mapandgametype";
Description = _translationLookup["COMMANDS_MAG_DESCRIPTION"];
Alias = "mag";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMADS_MAG_ARG_1"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMADS_MAG_ARG_2"],
Required = true
}
};
_logger = logger;
_defaultSettings = defaultSettings;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var match = Regex.Match(gameEvent.Data.Trim(), ArgumentRegexPattern,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
if (!match.Success)
{
gameEvent.Origin.Tell(Syntax);
return;
}
var map = match.Groups[1].Length > 0 ? match.Groups[1].ToString() : match.Groups[2].ToString();
var gametype = match.Groups[3].Length > 0 ? match.Groups[3].ToString() : match.Groups[4].ToString();
var matchingMaps = gameEvent.Owner.FindMap(map);
var matchingGametypes = _defaultSettings.FindGametype(gametype, gameEvent.Owner.GameName);
if (matchingMaps.Count > 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_MAG_MULTIPLE_MAPS"]);
foreach (var matchingMap in matchingMaps)
{
gameEvent.Origin.Tell(
$"[(Color::Yellow){matchingMap.Alias}(Color::White)] [(Color::Yellow){matchingMap.Name}(Color::White)]");
}
return;
}
if (matchingGametypes.Count > 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_MAG_MULTIPLE_GAMETYPES"]);
foreach (var matchingGametype in matchingGametypes)
{
gameEvent.Origin.Tell(
$"[(Color::Yellow){matchingGametype.Alias}(Color::White)] [(Color::Yellow){matchingGametype.Name}(Color::White)]");
}
return;
}
map = matchingMaps.FirstOrDefault()?.Name ?? map;
gametype = matchingGametypes.FirstOrDefault()?.Name ?? gametype;
var hasMatchingGametype = matchingGametypes.Any();
_logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype);
await gameEvent.Owner.SetDvarAsync("g_gametype", gametype, gameEvent.Owner.Manager.CancellationToken);
gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map));
await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds);
switch (gameEvent.Owner.GameName)
{
case Server.Game.IW5:
await gameEvent.Owner.ExecuteCommandAsync(
$"load_dsr {(hasMatchingGametype ? gametype.ToUpper() + "_default" : gametype)}");
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
case Server.Game.T6:
await gameEvent.Owner.ExecuteCommandAsync($"exec {gametype}.cfg");
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
default:
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
}
}
}
}

View File

@ -0,0 +1,135 @@
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;
namespace IW4MAdmin.Application.Commands
{
public class OfflineMessageCommand : Command
{
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, 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)
{
if (gameEvent.Data.Length > MaxLength)
{
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));
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
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
ServerId = server.Id,
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);
await context.SaveChangesAsync();
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SUCCESS"]);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not save offline message {@Message}", newMessage);
throw;
}
}
}
}

View File

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Sends a private message to another player
/// </summary>
public class PrivateMessageCommand : Command
{
public PrivateMessageCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "privatemessage";
Description = _translationLookup["COMMANDS_PM_DESC"];
Alias = "pm";
Permission = EFClient.Permission.User;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Target.Tell(_translationLookup["COMMANDS_PRIVATE_MESSAGE_FORMAT"].FormatExt(gameEvent.Origin.Name, gameEvent.Data));
gameEvent.Origin.Tell(_translationLookup["COMMANDS_PRIVATE_MESSAGE_RESULT"]
.FormatExt(gameEvent.Target.Name, gameEvent.Data));
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using EFClient = Data.Models.Client.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class ReadMessageCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public ReadMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
{
Name = "readmessage";
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
Alias = "rm";
Permission = EFClient.Permission.User;
_contextFactory = contextFactory;
_logger = logger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
try
{
await using var context = _contextFactory.CreateContext();
var inboxItems = await context.InboxMessages
.Include(message => message.SourceClient)
.ThenInclude(client => client.CurrentAlias)
.Where(message => message.DestinationClientId == gameEvent.Origin.ClientId)
.Where(message => !message.IsDelivered)
.ToListAsync();
if (!inboxItems.Any())
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_NONE"]);
return;
}
await gameEvent.Origin.TellAsync(inboxItems.Select((inboxItem, index) =>
{
var header = _translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
.FormatExt($"{index + 1}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name);
return new[] { header }.Union(inboxItem.Message.FragmentMessageForDisplay());
}).SelectMany(item => item));
inboxItems.ForEach(item => { item.IsDelivered = true; });
context.UpdateRange(inboxItems);
await context.SaveChangesAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Could not retrieve offline messages for {Client}", gameEvent.Origin.ToString());
throw;
}
}
}
}

View File

@ -0,0 +1,77 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Report client for given reason
/// </summary>
public class ReportClientCommand : Command
{
public ReportClientCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "report";
Description = _translationLookup["COMMANDS_REPORT_DESC"];
Alias = "rep";
Permission = EFClient.Permission.User;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent commandEvent)
{
if (commandEvent.Data.ToLower().Contains("camp"))
{
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_CAMP"]);
return;
}
var success = false;
switch ((await commandEvent.Target.Report(commandEvent.Data, commandEvent.Origin)
.WaitAsync(Utilities.DefaultCommandTimeout, commandEvent.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_SUCCESS"]);
success = true;
break;
case GameEvent.EventFailReason.Exception:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_DUPLICATE"]);
break;
case GameEvent.EventFailReason.Permission:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL"]
.FormatExt(commandEvent.Target.Name));
break;
case GameEvent.EventFailReason.Invalid:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_SELF"]);
break;
case GameEvent.EventFailReason.Throttle:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_TOOMANY"]);
break;
}
if (success)
{
commandEvent.Owner.ToAdmins(
$"(Color::Accent){commandEvent.Origin.Name}(Color::White) -> (Color::Red){commandEvent.Target.Name}(Color::White): {commandEvent.Data}");
}
}
}
}

View File

@ -0,0 +1,46 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints out a message to all clients on all servers
/// </summary>
public class SayAllCommand : Command
{
public SayAllCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "sayall";
Description = _translationLookup["COMMANDS_SAY_ALL_DESC"];
Alias = "sa";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var message = $"(Color::Accent){gameEvent.Origin.Name}(Color::White) - (Color::Red){gameEvent.Data}";
foreach (var server in gameEvent.Owner.Manager.GetServers())
{
server.Broadcast(message, gameEvent.Origin);
}
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using EFClient = Data.Models.Client.EFClient;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints out a message to all clients on the server
/// </summary>
public class SayCommand : Command
{
public SayCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "say";
Description = _translationLookup["COMMANDS_SAY_DESC"];
Alias = "s";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Owner.Broadcast(
_translationLookup["COMMANDS_SAY_FORMAT"].FormatExt(gameEvent.Origin.Name, gameEvent.Data),
gameEvent.Origin);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,38 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints client information
/// </summary>
public class WhoAmICommand : Command
{
public WhoAmICommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "whoami";
Description = _translationLookup["COMMANDS_WHO_DESC"];
Alias = "who";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var you =
"[(Color::Yellow)#{{clientNumber}}(Color::White)] [(Color::Yellow)@{{clientId}}(Color::White)] [{{networkId}}] [{{ip}}] [(Color::Cyan){{level}}(Color::White){{tag}}(Color::White)] {{name}}"
.FormatExt(gameEvent.Origin.ClientNumber,
gameEvent.Origin.ClientId, gameEvent.Origin.GuidString,
gameEvent.Origin.IPAddressString, gameEvent.Origin.ClientPermission.Name,
string.IsNullOrEmpty(gameEvent.Origin.Tag) ? "" : $" {gameEvent.Origin.Tag}",
gameEvent.Origin.Name);
gameEvent.Origin.Tell(you);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,56 @@
{
"Serilog": {
"Using": [
"Serilog.Sinks.File"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "Log/IW4MAdmin-Application.log",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}",
"RestrictedToMinimumLevel": "Fatal"
}
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
],
"Destructure": [
{
"Name": "ToMaximumDepth",
"Args": {
"maximumDestructuringDepth": 4
}
},
{
"Name": "ToMaximumStringLength",
"Args": {
"maximumStringLength": 1000
}
},
{
"Name": "ToMaximumCollectionCount",
"Args": {
"maximumCollectionCount": 24
}
}
]
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Configuration
{
public class ScriptPluginConfiguration : Dictionary<string, Dictionary<string, object>>, IBaseConfiguration
{
public string Name() => nameof(ScriptPluginConfiguration);
public IBaseConfiguration Generate()
{
return new ScriptPluginConfiguration();
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Concurrent;
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application
{
public class CoreEventHandler : ICoreEventHandler
{
private const int MaxCurrentEvents = 25;
private readonly ILogger _logger;
private readonly SemaphoreSlim _onProcessingEvents = new(MaxCurrentEvents, MaxCurrentEvents);
private readonly ManualResetEventSlim _onEventReady = new(false);
private readonly ConcurrentQueue<(IManager, CoreEvent)> _runningEventTasks = new();
private CancellationToken _cancellationToken;
private int _activeTasks;
private static readonly GameEvent.EventType[] OverrideEvents =
{
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public CoreEventHandler(ILogger<CoreEventHandler> logger)
{
_logger = logger;
}
public void QueueEvent(IManager manager, CoreEvent coreEvent)
{
_runningEventTasks.Enqueue((manager, coreEvent));
_onEventReady.Set();
}
public void StartProcessing(CancellationToken token)
{
_cancellationToken = token;
while (!_cancellationToken.IsCancellationRequested)
{
_onEventReady.Reset();
try
{
_onProcessingEvents.Wait(_cancellationToken);
if (!_runningEventTasks.TryDequeue(out var coreEvent))
{
if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
{
_onProcessingEvents.Release(1);
}
_onEventReady.Wait(_cancellationToken);
continue;
}
_logger.LogDebug("Start processing event {Name} {SemaphoreCount} - {QueuedTasks}",
coreEvent.Item2.GetType().Name, _onProcessingEvents.CurrentCount, _runningEventTasks.Count);
_ = Task.Factory.StartNew(() =>
{
Interlocked.Increment(ref _activeTasks);
_logger.LogDebug("[Start] Active Tasks = {TaskCount}", _activeTasks);
return HandleEventTaskExecute(coreEvent);
});
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not enqueue event for processing");
}
}
}
private async Task HandleEventTaskExecute((IManager, CoreEvent) coreEvent)
{
try
{
await GetEventTask(coreEvent.Item1, coreEvent.Item2);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Event timed out {Type}", coreEvent.Item2.GetType().Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete invoke for {EventType}",
coreEvent.Item2.GetType().Name);
}
finally
{
if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
{
_logger.LogDebug("Freeing up event semaphore for next event {SemaphoreCount}",
_onProcessingEvents.CurrentCount);
_onProcessingEvents.Release(1);
}
Interlocked.Decrement(ref _activeTasks);
_logger.LogDebug("[Complete] {Type}, Active Tasks = {TaskCount} - {Queue}", coreEvent.Item2.GetType(),
_activeTasks, _runningEventTasks.Count);
}
}
private Task GetEventTask(IManager manager, CoreEvent coreEvent)
{
return coreEvent switch
{
GameEvent gameEvent => BuildLegacyEventTask(manager, coreEvent, gameEvent),
GameServerEvent gameServerEvent => IGameServerEventSubscriptions.InvokeEventAsync(gameServerEvent,
manager.CancellationToken),
ManagementEvent managementEvent => IManagementEventSubscriptions.InvokeEventAsync(managementEvent,
manager.CancellationToken),
_ => Task.CompletedTask
};
}
private async Task BuildLegacyEventTask(IManager manager, CoreEvent coreEvent, GameEvent gameEvent)
{
if (manager.IsRunning || OverrideEvents.Contains(gameEvent.Type))
{
await manager.ExecuteEvent(gameEvent);
await IGameEventSubscriptions.InvokeEventAsync(coreEvent, manager.CancellationToken);
return;
}
_logger.LogDebug("Skipping event as we're shutting down {EventId}", gameEvent.IncrementalId);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,694 @@
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using Data.Models;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Game;
using static System.Int32;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers
{
public class BaseEventParser : IEventParser
{
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>
_customEventRegistrations;
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger,
ApplicationConfiguration appConfig)
{
_customEventRegistrations =
new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger;
_appConfig = appConfig;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
GameDirectory = "main",
LocalizeText = "\x15",
};
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5);
Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$";
Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$";
Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.JoinTeam.Pattern = @"^(JT);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(\w+);(.+)$";
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginTeam, 4);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginName, 5);
Configuration.Damage.Pattern =
@"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetName, 5);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginName, 9);
Configuration.Damage.AddMapping(ParserRegex.GroupType.Weapon, 10);
Configuration.Damage.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern =
@"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetName, 5);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginName, 9);
Configuration.Kill.AddMapping(ParserRegex.GroupType.Weapon, 10);
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.MapChange.Pattern = @".*InitGame.*";
Configuration.MapEnd.Pattern = @".*(?:ExitLevel|ShutdownGame).*";
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
_regexMap = new Dictionary<ParserRegex, GameEvent.EventType>
{
{ Configuration.Say, GameEvent.EventType.Say },
{ Configuration.Kill, GameEvent.EventType.Kill },
{ Configuration.MapChange, GameEvent.EventType.MapChange },
{ Configuration.MapEnd, GameEvent.EventType.MapEnd },
{ Configuration.JoinTeam, GameEvent.EventType.JoinTeam }
};
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
{
{ "say", GameEvent.EventType.Say },
{ "sayteam", GameEvent.EventType.SayTeam },
{ "chat", GameEvent.EventType.Say },
{ "chatteam", GameEvent.EventType.SayTeam },
{ "K", GameEvent.EventType.Kill },
{ "D", GameEvent.EventType.Damage },
{ "J", GameEvent.EventType.PreConnect },
{ "JT", GameEvent.EventType.JoinTeam },
{ "Q", GameEvent.EventType.PreDisconnect }
};
}
public IEventParserConfiguration Configuration { get; set; }
public string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
public string Name { get; set; } = "Call of Duty";
public virtual GameEvent GenerateGameEvent(string logLine)
{
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
var gameTime = 0L;
if (timeMatch.Success)
{
if (timeMatch.Values[0].Contains(":"))
{
gameTime = timeMatch
.Values
.Skip(2)
// this converts the timestamp into seconds passed
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
}
else
{
gameTime = long.Parse(timeMatch.Values[0]);
}
// we want to strip the time from the log line
logLine = logLine[timeMatch.Values.First().Length..].Trim();
}
var (eventType, eventKey) = GetEventTypeFromLine(logLine);
switch (eventType)
{
case GameEvent.EventType.Say or GameEvent.EventType.SayTeam:
return ParseMessageEvent(logLine, gameTime, eventType) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.Kill:
return ParseKillEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.Damage:
return ParseDamageEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.PreConnect:
return ParseClientEnterMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.JoinTeam:
return ParseJoinTeamEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.PreDisconnect:
return ParseClientExitMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.MapEnd:
return ParseMatchEndEvent(logLine, gameTime);
case GameEvent.EventType.MapChange:
return ParseMatchStartEvent(logLine, gameTime);
}
if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey))
{
return GenerateDefaultEvent(logLine, gameTime);
}
var eventModifier = _customEventRegistrations[eventKey];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
Type = GameEvent.EventType.Other,
Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not handle custom log event generation");
}
return GenerateDefaultEvent(logLine, gameTime);
}
private static GameEvent GenerateDefaultEvent(string logLine, long gameTime)
{
return new GameEvent
{
Type = GameEvent.EventType.Unknown,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
private static GameEvent ParseMatchStartEvent(string logLine, long gameTime)
{
var dump = logLine.Replace("InitGame: ", "").DictionaryFromKeyValue();
return new MatchStartEvent
{
Type = GameEvent.EventType.MapChange,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump,
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
SessionData = dump
};
}
private static GameEvent ParseMatchEndEvent(string logLine, long gameTime)
{
return new MatchEndEvent
{
Type = GameEvent.EventType.MapEnd,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
SessionData = logLine
};
}
private GameEvent ParseClientExitMatchEvent(string logLine, long gameTime)
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString =
match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var originClientNumber =
Convert.ToInt32(
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientExitMatchEvent
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = originClientNumber,
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber
};
}
private GameEvent ParseJoinTeamEvent(string logLine, long gameTime)
{
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString =
match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var clientSlotNumber =
Parse(match.Values[
Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (Configuration.TeamMapping.ContainsKey(team))
{
team = Configuration.TeamMapping[team].ToString();
}
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientJoinTeamEvent
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = clientSlotNumber,
State = EFClient.ClientState.Connected,
},
Extra = team,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
TeamName = team,
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientSlotNumber
};
}
private GameEvent ParseClientEnterMatchEvent(string logLine, long gameTime)
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]]
.TrimNewLine();
var originClientNumber =
Convert.ToInt32(
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientEnterMatchEvent
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = originClientNumber,
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber
};
}
#region DAMAGE
private GameEvent ParseDamageEvent(string logLine, long gameTime)
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]]
?.TrimNewLine();
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid()
? targetName.GenerateGuidFromString()
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber =
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber =
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originTeamName =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var targetTeamName =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetTeam]];
if (Configuration.TeamMapping.ContainsKey(originTeamName))
{
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
}
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
{
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
}
var weaponName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Weapon]];
TryParse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Damage]],
out var damage);
var meansOfDeath =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
var hitLocation =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.HitLocation]];
return new ClientDamageEvent
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber,
AttackerTeamName = originTeamName,
VictimClientName = targetName,
VictimNetworkId = targetIdString,
VictimClientSlotNumber = targetClientNumber,
VictimTeamName = targetTeamName,
WeaponName = weaponName,
Damage = damage,
MeansOfDeath = meansOfDeath,
HitLocation = hitLocation
};
}
private GameEvent ParseKillEvent(string logLine, long gameTime)
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]]
?.TrimNewLine();
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid()
? targetName.GenerateGuidFromString()
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber =
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber =
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originTeamName =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var targetTeamName =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetTeam]];
if (Configuration.TeamMapping.ContainsKey(originTeamName))
{
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
}
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
{
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
}
var weaponName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Weapon]];
TryParse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Damage]],
out var damage);
var meansOfDeath =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
var hitLocation =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.HitLocation]];
return new ClientKillEvent
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber,
AttackerTeamName = originTeamName,
VictimClientName = targetName,
VictimNetworkId = targetIdString,
VictimClientSlotNumber = targetClientNumber,
VictimTeamName = targetTeamName,
WeaponName = weaponName,
Damage = damage,
MeansOfDeath = meansOfDeath,
HitLocation = hitLocation
};
}
#endregion
#region MESSAGE
private GameEvent ParseMessageEvent(string logLine, long gameTime, GameEvent.EventType eventType)
{
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (!matchResult.Success)
{
return null;
}
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Replace(Configuration.LocalizeText, "")
.Trim();
if (message.Length <= 0)
{
return null;
}
var originIdString =
matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var clientNumber =
Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
return new ClientCommandEvent
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
//V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientNumber,
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
};
}
return new ClientMessageEvent
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
//V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientNumber,
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
};
}
#endregion
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
{
var lineSplit = logLine.Split(';');
if (lineSplit.Length > 1)
{
var type = lineSplit[0];
return _eventTypeMap.ContainsKey(type)
? (_eventTypeMap[type], type)
: (GameEvent.EventType.Unknown, lineSplit[0]);
}
foreach (var (key, value) in _regexMap)
{
var result = key.PatternMatcher.Match(logLine);
if (result.Success)
{
return (value, null);
}
}
return (GameEvent.EventType.Unknown, null);
}
/// <inheritdoc/>
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue,
Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
{
if (string.IsNullOrWhiteSpace(eventSubtype))
{
throw new ArgumentException("Event subtype cannot be empty");
}
if (string.IsNullOrWhiteSpace(eventTriggerValue))
{
throw new ArgumentException("Event trigger value cannot be empty");
}
if (eventModifier == null)
{
throw new ArgumentException("Event modifier must be specified");
}
if (_customEventRegistrations.ContainsKey(eventTriggerValue))
{
throw new ArgumentException($"Event trigger value '{eventTriggerValue}' is already registered");
}
_customEventRegistrations.Add(eventTriggerValue, (eventSubtype, eventModifier));
}
}
}

View File

@ -0,0 +1,17 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// empty generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic event parsers
/// </summary>
sealed internal class DynamicEventParser : BaseEventParser
{
public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) : base(parserRegexFactory, logger, appConfig)
{
}
}
}

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
using System.Globalization;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic configurations
/// </summary>
internal sealed class DynamicEventParserConfiguration : IEventParserConfiguration
{
public string GameDirectory { get; set; }
public ParserRegex Say { get; set; }
public string LocalizeText { get; set; }
public ParserRegex Join { get; set; }
public ParserRegex JoinTeam { get; set; }
public ParserRegex Quit { get; set; }
public ParserRegex Kill { get; set; }
public ParserRegex Damage { get; set; }
public ParserRegex Action { get; set; }
public ParserRegex Time { get; set; }
public ParserRegex MapChange { get; set; }
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();
Join = parserRegexFactory.CreateParserRegex();
JoinTeam = parserRegexFactory.CreateParserRegex();
Quit = parserRegexFactory.CreateParserRegex();
Kill = parserRegexFactory.CreateParserRegex();
Damage = parserRegexFactory.CreateParserRegex();
Action = parserRegexFactory.CreateParserRegex();
Time = parserRegexFactory.CreateParserRegex();
MapChange = parserRegexFactory.CreateParserRegex();
MapEnd = parserRegexFactory.CreateParserRegex();
}
}
}

View File

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.EventParsers
{
class IW3EventParser : IW4EventParser
{
public override string GetGameDir() => "main";
}
}

View File

@ -1,155 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.EventParsers
{
class IW4EventParser : IEventParser
{
public virtual GameEvent GetEvent(Server server, string logLine)
{
string[] lineSplit = logLine.Split(';');
string cleanedEventLine = Regex.Replace(lineSplit[0], @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
if (cleanedEventLine[0] == 'K')
{
if (!server.CustomCallback)
{
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
}
if (cleanedEventLine == "say" || cleanedEventLine == "sayteam")
{
string message = lineSplit[4].Replace("\x15", "");
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = message
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = message
};
}
if (cleanedEventLine.Contains("ScriptKill"))
{
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
Owner = server
};
}
if (cleanedEventLine.Contains("ScriptDamage"))
{
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
Owner = server
};
}
if (cleanedEventLine[0] == 'D')
{
if (Regex.Match(cleanedEventLine, @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$").Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = cleanedEventLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Owner = server
};
}
}
if (cleanedEventLine.Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
if (cleanedEventLine.Contains("InitGame"))
{
string dump = cleanedEventLine.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server,
Extra = dump.DictionaryFromKeyValue()
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
// other parsers can derive from this parser so we make it virtual
public virtual string GetGameDir() => "userraw";
}
}

View File

@ -1,53 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.EventParsers
{
class IW5EventParser : IW4EventParser
{
public override string GetGameDir() => "logs";
public override GameEvent GetEvent(Server server, string logLine)
{
string cleanedEventLine = Regex.Replace(logLine, @"[0-9]+:[0-9]+\ ", "").Trim();
if (cleanedEventLine.Contains("J;"))
{
string[] lineSplit = cleanedEventLine.Split(';');
int clientNum = Int32.Parse(lineSplit[2]);
var player = new Player()
{
NetworkId = lineSplit[1].ConvertLong(),
ClientNumber = clientNum,
Name = lineSplit[3]
};
return new GameEvent()
{
Type = GameEvent.EventType.Join,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server,
Extra = player
};
}
else
return base.GetEvent(server, logLine);
}
}
}

View File

@ -0,0 +1,35 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// implementation of the IParserPatternMatcher for windows (really it's the only implementation)
/// </summary>
public class ParserPatternMatcher : IParserPatternMatcher
{
private Regex regex;
/// <inheritdoc/>
public void Compile(string pattern)
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
/// <inheritdoc/>
public IMatchResult Match(string input)
{
var match = regex.Match(input);
return new ParserMatchResult()
{
Success = match.Success,
Values = (match.Groups as IEnumerable<object>)?
.Select(_item => _item.ToString()).ToArray() ?? new string[0]
};
}
}
}

View File

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.EventParsers
{
class T5MEventParser : IW4EventParser
{
public override string GetGameDir() => "v2";
}
}

View File

@ -1,111 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.EventParsers
{
class T6MEventParser : IW4EventParser
{
/*public GameEvent GetEvent(Server server, string logLine)
{
string cleanedEventLine = Regex.Replace(logLine, @"^ *[0-9]+:[0-9]+ *", "").Trim();
string[] lineSplit = cleanedEventLine.Split(';');
if (lineSplit[0][0] == 'K')
{
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = cleanedEventLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
if (lineSplit[0][0] == 'D')
{
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = cleanedEventLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
if (lineSplit[0] == "say" || lineSplit[0] == "sayteam")
{
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = lineSplit[4],
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = lineSplit[4]
};
}
if (lineSplit[0].Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
if (lineSplit[0].Contains("InitGame"))
{
string dump = cleanedEventLine.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server,
Extra = dump.DictionaryFromKeyValue()
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}*/
public override string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
using System.Linq;
using IW4MAdmin.Application.Plugin.Script;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Extensions
{
public static class CommandExtensions
{
/// <summary>
/// determines the command configuration name for given manager command
/// </summary>
/// <param name="command">command to determine config name for</param>
/// <returns></returns>
public static string CommandConfigNameForType(this IManagerCommand command)
{
return command.GetType() == typeof(ScriptCommand)
? $"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command"
: command.GetType().Name;
}
public static IList<Map> FindMap(this Server server, string mapName) => server.Maps.Where(map =>
map.Name.Equals(mapName, StringComparison.InvariantCultureIgnoreCase) ||
map.Alias.Equals(mapName, StringComparison.InvariantCultureIgnoreCase)).ToList();
public static IList<Gametype> FindGametype(this DefaultSettings settings, string gameType, Server.Game? game = null) =>
settings.Gametypes?.Where(gt => game == null || gt.Game == game)
.SelectMany(gt => gt.Gametypes).Where(gt =>
gt.Alias.Contains(gameType, StringComparison.CurrentCultureIgnoreCase) ||
gt.Name.Contains(gameType, StringComparison.CurrentCultureIgnoreCase)).ToList();
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
namespace IW4MAdmin.Application.Extensions;
public static class ScriptPluginExtensions
{
public static IEnumerable<object> GetClientsBasicData(
this DbSet<Data.Models.Client.EFClient> set, int[] clientIds)
{
return set.Where(client => clientIds.Contains(client.ClientId))
.Select(client => new
{
client.ClientId,
client.CurrentAlias,
client.Level,
client.NetworkId
}).ToList();
}
public static IEnumerable<object> GetClientsStatData(this DbSet<EFClientStatistics> set, int[] clientIds,
double serverId)
{
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Data.MigrationContext;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using ILogger = Serilog.ILogger;
namespace IW4MAdmin.Application.Extensions
{
public static class StartupExtensions
{
private static ILogger _defaultLogger = null;
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
if (_defaultLogger == null)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(Path.Join(Utilities.OperatingDirectory, "Configuration", "LoggingConfiguration.json"))
.Build();
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
if (Utilities.IsDevelopment)
{
loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate:
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Debug();
}
_defaultLogger = loggerConfig.CreateLogger();
}
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
services.AddSingleton(new LoggerFactory()
.AddSerilog(_defaultLogger, true));
return services;
}
public static IServiceCollection AddDatabaseContextOptions(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
var activeProvider = appConfig.DatabaseProvider?.ToLower();
if (string.IsNullOrEmpty(appConfig.ConnectionString) || activeProvider == "sqlite")
{
var currentPath = Utilities.OperatingDirectory;
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? $"{Path.DirectorySeparatorChar}{currentPath}"
: currentPath;
var connectionStringBuilder = new SqliteConnectionStringBuilder
{DataSource = Path.Join(currentPath, "Database", "Database.db")};
var connectionString = connectionStringBuilder.ToString();
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<SqliteDatabaseContext>()
.UseSqlite(connectionString)
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>())
.EnableSensitiveDataLogging().Options);
return services;
}
switch (activeProvider)
{
case "mysql":
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
StringComparison.InvariantCultureIgnoreCase);
var connectionString =
appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : "");
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<MySqlDatabaseContext>()
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
mysqlOptions => mysqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
case "postgresql":
appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
StringComparison.InvariantCultureIgnoreCase);
services.AddSingleton(sp =>
(DbContextOptions) new DbContextOptionsBuilder<PostgresqlDatabaseContext>()
.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
postgresqlOptions =>
{
postgresqlOptions.EnableRetryOnFailure();
postgresqlOptions.SetPostgresVersion(new Version("12.9"));
})
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
default:
throw new ArgumentException($"No context available for {appConfig.DatabaseProvider}");
}
}
}
}

View File

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IConfigurationHandlerFactory
/// provides base functionality to create configuration handlers
/// </summary>
public class ConfigurationHandlerFactory : IConfigurationHandlerFactory
{
/// <summary>
/// creates a base configuration handler
/// </summary>
/// <typeparam name="T">base configuration type</typeparam>
/// <param name="name">name of the config file</param>
/// <returns></returns>
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration
{
var handler = new BaseConfigurationHandler<T>(name);
handler.BuildAsync().Wait();
return handler;
}
/// <inheritdoc/>
public async Task<IConfigurationHandler<T>> GetConfigurationHandlerAsync<T>(string name) where T : IBaseConfiguration
{
var handler = new BaseConfigurationHandler<T>(name);
await handler.BuildAsync();
return handler;
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using Data.Abstractions;
using Data.Context;
using Data.MigrationContext;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of the IDatabaseContextFactory interface
/// </summary>
public class DatabaseContextFactory : IDatabaseContextFactory
{
private readonly DbContextOptions _contextOptions;
private readonly string _activeProvider;
public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
{
_contextOptions = contextOptions;
_activeProvider = appConfig.DatabaseProvider?.ToLower();
}
/// <summary>
/// creates a new database context
/// </summary>
/// <param name="enableTracking">indicates if entity tracking should be enabled</param>
/// <returns></returns>
public DatabaseContext CreateContext(bool? enableTracking = true)
{
var context = BuildContext();
enableTracking ??= true;
if (enableTracking.Value)
{
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.ChangeTracker.LazyLoadingEnabled = true;
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
}
else
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
context.ChangeTracker.LazyLoadingEnabled = false;
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
return context;
}
private DatabaseContext BuildContext()
{
return _activeProvider switch
{
"sqlite" => new SqliteDatabaseContext(_contextOptions),
"mysql" => new MySqlDatabaseContext(_contextOptions),
"postgresql" => new PostgresqlDatabaseContext(_contextOptions),
_ => throw new ArgumentException($"No context found for {_activeProvider}")
};
}
}
}

View File

@ -0,0 +1,42 @@
using IW4MAdmin.Application.IO;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Interfaces;
using System;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories
{
public class GameLogReaderFactory : IGameLogReaderFactory
{
private readonly IServiceProvider _serviceProvider;
public GameLogReaderFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IGameLogReader CreateGameLogReader(Uri[] logUris, IEventParser eventParser)
{
var baseUri = logUris[0];
if (baseUri.Scheme == Uri.UriSchemeHttp || baseUri.Scheme == Uri.UriSchemeHttps)
{
return new GameLogReaderHttp(logUris, eventParser,
_serviceProvider.GetRequiredService<ILogger<GameLogReaderHttp>>());
}
if (baseUri.Scheme == Uri.UriSchemeFile)
{
return new GameLogReader(baseUri.LocalPath, eventParser,
_serviceProvider.GetRequiredService<ILogger<GameLogReader>>());
}
if (baseUri.Scheme == Uri.UriSchemeNetTcp)
{
return new NetworkGameLogReader(logUris, eventParser,
_serviceProvider.GetRequiredService<ILogger<NetworkGameLogReader>>());
}
throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\"");
}
}
}

View File

@ -0,0 +1,48 @@
using System;
using Data.Abstractions;
using Data.Models.Server;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IGameServerInstanceFactory
/// </summary>
internal class GameServerInstanceFactory : IGameServerInstanceFactory
{
private readonly ITranslationLookup _translationLookup;
private readonly IMetaServiceV2 _metaService;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// base constructor
/// </summary>
/// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup,
IMetaServiceV2 metaService,
IServiceProvider serviceProvider)
{
_translationLookup = translationLookup;
_metaService = metaService;
_serviceProvider = serviceProvider;
}
/// <summary>
/// creates an IW4MServer instance
/// </summary>
/// <param name="config">server configuration</param>
/// <param name="manager">application manager</param>
/// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager)
{
return new IW4MServer(config,
_serviceProvider.GetRequiredService<CommandConfiguration>(), _translationLookup, _metaService,
_serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(),
_serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
}
}
}

View File

@ -0,0 +1,26 @@
using SharedLibraryCore.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// Implementation of the IParserRegexFactory
/// </summary>
public class ParserRegexFactory : IParserRegexFactory
{
private readonly IServiceProvider _serviceProvider;
/// <inheritdoc/>
public ParserRegexFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public ParserRegex CreateParserRegex()
{
return new ParserRegex(_serviceProvider.GetService<IParserPatternMatcher>());
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Net;
using SharedLibraryCore.Interfaces;
using System.Text;
using Integrations.Cod;
using Integrations.Source;
using Integrations.Source.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IRConConnectionFactory
/// </summary>
internal class RConConnectionFactory : IRConConnectionFactory
{
private static readonly Encoding GameEncoding = Encoding.GetEncoding("windows-1252");
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Base constructor
/// </summary>
/// <param name="logger"></param>
public RConConnectionFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public IRConConnection CreateConnection(IPEndPoint ipEndpoint, string password, string rconEngine)
{
return rconEngine switch
{
"COD" => new CodRConConnection(ipEndpoint, password,
_serviceProvider.GetRequiredService<ILogger<CodRConConnection>>(), GameEncoding,
_serviceProvider.GetRequiredService<ApplicationConfiguration>()?.ServerConnectionAttempts ?? 6),
"Source" => new SourceRConConnection(
_serviceProvider.GetRequiredService<ILogger<SourceRConConnection>>(),
_serviceProvider.GetRequiredService<IRConClientFactory>(), ipEndpoint, password),
_ => throw new ArgumentException($"No supported RCon engine available for '{rconEngine}'")
};
}
}
}

View File

@ -0,0 +1,42 @@
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IScriptCommandFactory
/// </summary>
public class ScriptCommandFactory : IScriptCommandFactory
{
private readonly CommandConfiguration _config;
private readonly ITranslationLookup _transLookup;
private readonly IServiceProvider _serviceProvider;
public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup, IServiceProvider serviceProvider)
{
_config = config;
_transLookup = transLookup;
_serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
bool isTargetRequired, IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, IEnumerable<Reference.Game> supportedGames)
{
var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, args, executeAction,
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
}
}
}

View File

@ -1,105 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
private ConcurrentQueue<GameEvent> EventQueue;
private Queue<GameEvent> StatusSensitiveQueue;
private IManager Manager;
public GameEventHandler(IManager mgr)
{
EventQueue = new ConcurrentQueue<GameEvent>();
StatusSensitiveQueue = new Queue<GameEvent>();
Manager = mgr;
}
public void AddEvent(GameEvent gameEvent)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}");
#endif
// we need this to keep accurate track of the score
if (gameEvent.Type == GameEvent.EventType.Kill ||
gameEvent.Type == GameEvent.EventType.Damage ||
gameEvent.Type == GameEvent.EventType.ScriptDamage ||
gameEvent.Type == GameEvent.EventType.ScriptKill ||
gameEvent.Type == GameEvent.EventType.MapChange)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Added sensitive event to queue");
#endif
lock (StatusSensitiveQueue)
{
StatusSensitiveQueue.Enqueue(gameEvent);
}
return;
}
else
{
EventQueue.Enqueue(gameEvent);
Manager.SetHasEvent();
}
#if DEBUG
Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue");
#endif
}
public string[] GetEventOutput()
{
throw new NotImplementedException();
}
public GameEvent GetNextSensitiveEvent()
{
if (StatusSensitiveQueue.Count > 0)
{
lock (StatusSensitiveQueue)
{
if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent))
{
Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing");
}
else
{
return newEvent;
}
}
}
return null;
}
public GameEvent GetNextEvent()
{
if (EventQueue.Count > 0)
{
#if DEBUG
Manager.GetLogger().WriteDebug("Getting next event to be processed");
#endif
if (!EventQueue.TryDequeue(out GameEvent newEvent))
{
Manager.GetLogger().WriteWarning("Could not dequeue event for processing");
}
else
{
return newEvent;
}
}
return null;
}
}
}

View File

@ -0,0 +1,210 @@
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.IO;
public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHandlerV2<TConfigurationType>
where TConfigurationType : class
{
private readonly ILogger<BaseConfigurationHandlerV2<TConfigurationType>> _logger;
private readonly ConfigurationWatcher _watcher;
private readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter()
},
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private readonly SemaphoreSlim _onIo = new(1, 1);
private TConfigurationType _configurationInstance;
private string _path = string.Empty;
private event Action<string> FileUpdated;
public BaseConfigurationHandlerV2(ILogger<BaseConfigurationHandlerV2<TConfigurationType>> logger,
ConfigurationWatcher watcher)
{
_logger = logger;
_watcher = watcher;
FileUpdated += OnFileUpdated;
}
~BaseConfigurationHandlerV2()
{
FileUpdated -= OnFileUpdated;
_watcher.Unregister(_path);
}
public async Task<TConfigurationType> Get(string configurationName,
TConfigurationType defaultConfiguration = default)
{
if (string.IsNullOrWhiteSpace(configurationName))
{
return defaultConfiguration;
}
var cleanName = configurationName.Replace("\\", "").Replace("/", "");
if (string.IsNullOrWhiteSpace(configurationName))
{
return defaultConfiguration;
}
_path = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{cleanName}.json");
TConfigurationType readConfiguration = null;
try
{
await _onIo.WaitAsync();
await using var fileStream = File.OpenRead(_path);
readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
_watcher.Register(_path, FileUpdated);
if (readConfiguration is null)
{
_logger.LogError("Could not parse configuration {Type} at {FileName}", typeof(TConfigurationType).Name,
_path);
return defaultConfiguration;
}
}
catch (FileNotFoundException)
{
if (defaultConfiguration is not null)
{
await InternalSet(defaultConfiguration, false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not read configuration file at {Path}", _path);
return defaultConfiguration;
}
finally
{
if (_onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
return _configurationInstance ??= readConfiguration;
}
public async Task Set(TConfigurationType configuration)
{
await InternalSet(configuration, true);
}
public async Task Set()
{
if (_configurationInstance is not null)
{
await InternalSet(_configurationInstance, true);
}
}
private async Task InternalSet(TConfigurationType configuration, bool awaitSemaphore)
{
try
{
if (awaitSemaphore)
{
await _onIo.WaitAsync();
}
await using var fileStream = File.OpenWrite(_path);
await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions);
await fileStream.DisposeAsync();
_configurationInstance = configuration;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not save configuration {Type} {Path}", configuration.GetType().Name, _path);
}
finally
{
if (awaitSemaphore && _onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
}
private async void OnFileUpdated(string filePath)
{
try
{
await _onIo.WaitAsync();
await using var fileStream = File.OpenRead(_path);
var readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
if (readConfiguration is null)
{
_logger.LogWarning("Could not parse updated configuration {Type} at {Path}",
typeof(TConfigurationType).Name, filePath);
}
else
{
CopyUpdatedProperties(readConfiguration);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not parse updated configuration {Type} at {Path}",
typeof(TConfigurationType).Name, filePath);
}
finally
{
if (_onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
}
private void CopyUpdatedProperties(TConfigurationType newConfiguration)
{
if (_configurationInstance is null)
{
_configurationInstance = newConfiguration;
return;
}
_logger.LogDebug("Updating existing config with new values {Type} at {Path}", typeof(TConfigurationType).Name,
_path);
if (_configurationInstance is IDictionary configDict && newConfiguration is IDictionary newConfigDict)
{
configDict.Clear();
foreach (var key in newConfigDict.Keys)
{
configDict.Add(key, newConfigDict[key]);
}
}
else
{
foreach (var property in _configurationInstance.GetType().GetProperties()
.Where(prop => prop.CanRead && prop.CanWrite))
{
property.SetValue(_configurationInstance, property.GetValue(newConfiguration));
}
}
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharedLibraryCore;
namespace IW4MAdmin.Application.IO;
public sealed class ConfigurationWatcher : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly Dictionary<string, Action<string>> _registeredActions = new();
public ConfigurationWatcher()
{
_watcher = new FileSystemWatcher
{
Path = Path.Join(Utilities.OperatingDirectory, "Configuration"),
Filter = "*.json",
NotifyFilter = NotifyFilters.LastWrite
};
_watcher.Changed += WatcherOnChanged;
_watcher.EnableRaisingEvents = true;
}
public void Dispose()
{
_watcher.Changed -= WatcherOnChanged;
_watcher.Dispose();
}
public void Register(string fileName, Action<string> fileUpdated)
{
if (_registeredActions.ContainsKey(fileName))
{
return;
}
_registeredActions.Add(fileName, fileUpdated);
}
public void Unregister(string fileName)
{
if (_registeredActions.ContainsKey(fileName))
{
_registeredActions.Remove(fileName);
}
}
private void WatcherOnChanged(object sender, FileSystemEventArgs eventArgs)
{
if (!_registeredActions.ContainsKey(eventArgs.FullPath) || eventArgs.ChangeType != WatcherChangeTypes.Changed ||
new FileInfo(eventArgs.FullPath).Length == 0)
{
return;
}
_registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath);
}
}

View File

@ -1,78 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEvent
{
Server Server;
long PreviousFileSize;
GameLogReader Reader;
string GameLogFile;
class EventState
{
public ILogger Log { get; set; }
public string ServerId { get; set; }
}
public GameLogEvent(Server server, string gameLogPath, string gameLogName)
{
GameLogFile = gameLogPath;
Reader = new GameLogReader(gameLogPath, server.EventParser);
Server = server;
Task.Run(async () =>
{
while (!server.Manager.ShutdownRequested())
{
OnEvent(new EventState()
{
Log = server.Manager.GetLogger(),
ServerId = server.ToString()
});
await Task.Delay(100);
}
});
}
private void OnEvent(object state)
{
long newLength = new FileInfo(GameLogFile).Length;
try
{
UpdateLogEvents(newLength);
}
catch (Exception e)
{
((EventState)state).Log.WriteWarning($"Failed to update log event for {((EventState)state).ServerId}");
((EventState)state).Log.WriteDebug($"Exception: {e.Message}");
((EventState)state).Log.WriteDebug($"StackTrace: {e.StackTrace}");
}
}
private void UpdateLogEvents(long fileSize)
{
if (PreviousFileSize == 0)
PreviousFileSize = fileSize;
long fileDiff = fileSize - PreviousFileSize;
if (fileDiff < 1)
return;
PreviousFileSize = fileSize;
var events = Reader.EventsFromLog(Server, fileDiff, 0);
foreach (var ev in events)
Server.Manager.GetEventHandler().AddEvent(ev);
PreviousFileSize = fileSize;
}
}
}

View File

@ -0,0 +1,124 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
public class GameLogEventDetection
{
private long previousFileSize;
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly bool _ignoreBots;
private readonly ILogger _logger;
public GameLogEventDetection(ILogger<GameLogEventDetection> logger, IW4MServer server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
{
_reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser);
_server = server;
_ignoreBots = server.Manager.GetApplicationSettings().Configuration()?.IgnoreBots ?? false;
_logger = logger;
}
public async Task PollForChanges()
{
while (!_server.Manager.CancellationToken.IsCancellationRequested)
{
if (_server.IsInitialized)
{
try
{
await UpdateLogEvents();
}
catch (Exception e)
{
using(LogContext.PushProperty("Server", _server.ToString()))
{
_logger.LogError(e, "Failed to update log event for {endpoint}", _server.EndPoint);
}
}
}
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
}
_logger.LogDebug("Stopped polling for changes");
}
public async Task UpdateLogEvents()
{
long fileSize = _reader.Length;
if (previousFileSize == 0)
{
previousFileSize = fileSize;
}
long fileDiff = fileSize - previousFileSize;
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
{
previousFileSize = fileSize;
return;
}
var events = await _reader.ReadEventsFromLog(fileDiff, previousFileSize, _server);
foreach (var gameEvent in events)
{
try
{
gameEvent.Owner = _server;
// we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != Utilities.WORLD_ID)
{
gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);;
}
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
{
gameEvent.Target = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
}
if (gameEvent.Origin != null)
{
gameEvent.Origin.CurrentServer = _server;
}
if (gameEvent.Target != null)
{
gameEvent.Target.CurrentServer = _server;
}
_server.Manager.AddEvent(gameEvent);
}
}
catch (InvalidOperationException)
{
if (_ignoreBots)
{
continue;
}
using(LogContext.PushProperty("Server", _server.ToString()))
{
_logger.LogError("Could not find client in client list when parsing event line {data}", gameEvent.Data);
}
}
}
previousFileSize = fileSize;
}
}
}

View File

@ -3,58 +3,79 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
class GameLogReader
class GameLogReader : IGameLogReader
{
IEventParser Parser;
string LogFile;
private readonly IEventParser _parser;
private readonly string _logFile;
private readonly ILogger _logger;
public GameLogReader(string logFile, IEventParser parser)
public long Length => new FileInfo(_logFile).Length;
public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser, ILogger<GameLogReader> logger)
{
LogFile = logFile;
Parser = parser;
_logFile = logFile;
_parser = parser;
_logger = logger;
}
public ICollection<GameEvent> EventsFromLog(Server server, long fileSizeDiff, long startPosition)
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition, Server server = null)
{
// allocate the bytes for the new log lines
List<string> logLines = new List<string>();
// open the file as a stream
using (var rd = new StreamReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
using (FileStream fs = new FileStream(_logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
// take the old start position and go back the number of new characters
rd.BaseStream.Seek(-fileSizeDiff, SeekOrigin.End);
// the difference should be in the range of a int :P
string newLine;
while (!String.IsNullOrEmpty(newLine = rd.ReadLine()))
byte[] buff = new byte[fileSizeDiff];
fs.Seek(startPosition, SeekOrigin.Begin);
await fs.ReadAsync(buff, 0, (int)fileSizeDiff);
var stringBuilder = new StringBuilder();
char[] charBuff = Utilities.EncodingType.GetChars(buff);
foreach (char c in charBuff)
{
logLines.Add(newLine);
if (c == '\n')
{
logLines.Add(stringBuilder.ToString());
stringBuilder = new StringBuilder();
}
else if (c != '\r')
{
stringBuilder.Append(c);
}
}
if (stringBuilder.Length > 0)
{
logLines.Add(stringBuilder.ToString());
}
}
List<GameEvent> events = new List<GameEvent>();
// parse each line
foreach (string eventLine in logLines)
{
if (eventLine.Length > 0)
foreach (string eventLine in logLines.Where(_line => _line.Length > 0))
{
try
{
// todo: catch elsewhere
events.Add(Parser.GetEvent(server, eventLine));
var gameEvent = _parser.GenerateGameEvent(eventLine);
events.Add(gameEvent);
}
catch (Exception e)
{
Program.ServerManager.GetLogger().WriteWarning("Could not properly parse event line");
Program.ServerManager.GetLogger().WriteDebug(e.Message);
Program.ServerManager.GetLogger().WriteDebug(eventLine);
}
_logger.LogError(e, "Could not properly parse event line {@eventLine}", eventLine);
}
}

View File

@ -0,0 +1,75 @@
using IW4MAdmin.Application.API.GameLogServer;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capability of reading log files over HTTP
/// </summary>
class GameLogReaderHttp : IGameLogReader
{
private readonly IEventParser _eventParser;
private readonly IGameLogServer _logServerApi;
private readonly ILogger _logger;
private readonly string _safeLogPath;
private string lastKey = "next";
public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger<GameLogReaderHttp> logger)
{
_eventParser = parser;
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString());
_safeLogPath = gameLogServerUris[1].LocalPath.ToBase64UrlSafeString();
_logger = logger;
}
public long Length => -1;
public int UpdateInterval => 500;
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition, Server server = null)
{
var events = new List<GameEvent>();
var response = await _logServerApi.Log(_safeLogPath, lastKey);
lastKey = response.NextKey;
if (!response.Success && string.IsNullOrEmpty(lastKey))
{
_logger.LogError("Could not get log server info of {logPath}", _safeLogPath);
return events;
}
else if (!string.IsNullOrWhiteSpace(response.Data))
{
// parse each line
var lines = response.Data
.Split(Environment.NewLine)
.Where(_line => _line.Length > 0);
foreach (string eventLine in lines)
{
try
{
// this trim end should hopefully fix the nasty runaway regex
var gameEvent = _eventParser.GenerateGameEvent(eventLine.TrimEnd('\r'));
events.Add(gameEvent);
}
catch (Exception e)
{
_logger.LogError(e, "Could not properly parse event line from http {eventLine}", eventLine);
}
}
}
return events;
}
}
}

View File

@ -0,0 +1,163 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Integrations.Cod;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capability of reading log files over udp
/// </summary>
class NetworkGameLogReader : IGameLogReader
{
private readonly IEventParser _eventParser;
private readonly ILogger _logger;
private readonly Uri _uri;
private static readonly NetworkLogState State = new();
private bool _stateRegistered;
private CancellationToken _token;
public NetworkGameLogReader(IReadOnlyList<Uri> uris, IEventParser parser, ILogger<NetworkGameLogReader> logger)
{
_eventParser = parser;
_uri = uris[0];
_logger = logger;
}
public long Length => -1;
public int UpdateInterval => 150;
public Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition,
Server server = null)
{
// todo: other games might support this
var serverEndpoint = (server?.RemoteConnection as CodRConConnection)?.Endpoint;
if (serverEndpoint is null)
{
return Task.FromResult(Enumerable.Empty<GameEvent>());
}
if (!_stateRegistered && !State.EndPointExists(serverEndpoint))
{
try
{
var client = State.RegisterEndpoint(serverEndpoint, BuildLocalEndpoint()).Client;
_stateRegistered = true;
_token = server.Manager.CancellationToken;
if (client == null)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogInformation("Not registering {Name} socket because it is already bound",
nameof(NetworkGameLogReader));
}
return Task.FromResult(Enumerable.Empty<GameEvent>());
}
Task.Run(async () => await ReadNetworkData(client, _token), _token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not register {Name} endpoint {Endpoint}",
nameof(NetworkGameLogReader), _uri);
throw;
}
}
var events = new List<GameEvent>();
foreach (var logData in State.GetServerLogData(serverEndpoint)
.Select(log => Utilities.EncodingType.GetString(log)))
{
if (string.IsNullOrWhiteSpace(logData))
{
return Task.FromResult(Enumerable.Empty<GameEvent>());
}
var lines = logData
.Split('\n')
.Where(line => line.Length > 0 && !line.Contains('ÿ'));
foreach (var eventLine in lines)
{
try
{
// this trim end should hopefully fix the nasty runaway regex
var gameEvent = _eventParser.GenerateGameEvent(eventLine.TrimEnd('\r'));
events.Add(gameEvent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not properly parse event line from http {EventLine}",
eventLine);
}
}
}
return Task.FromResult((IEnumerable<GameEvent>)events);
}
private async Task ReadNetworkData(UdpClient client, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// get more data
IPEndPoint remoteEndpoint = null;
byte[] bufferedData = null;
if (client == null)
{
// we already have a socket listening on this port for data, so we don't need to run another thread
break;
}
try
{
var result = await client.ReceiveAsync(_token);
remoteEndpoint = result.RemoteEndPoint;
bufferedData = result.Buffer;
}
catch (OperationCanceledException)
{
_logger.LogDebug("Stopping network log receive");
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not receive lines for {LogReader}", nameof(NetworkGameLogReader));
}
if (bufferedData != null)
{
State.QueueServerLogData(remoteEndpoint, bufferedData);
}
}
}
private IPEndPoint BuildLocalEndpoint()
{
try
{
return new IPEndPoint(Dns.GetHostAddresses(_uri.Host).First(), _uri.Port);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not setup {LogReader} endpoint", nameof(NetworkGameLogReader));
throw;
}
}
}
}

View File

@ -0,0 +1,138 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace IW4MAdmin.Application.IO;
public class NetworkLogState : Dictionary<IPEndPoint, UdpClientState>
{
public UdpClientState RegisterEndpoint(IPEndPoint serverEndpoint, IPEndPoint localEndpoint)
{
try
{
lock (this)
{
if (!ContainsKey(serverEndpoint))
{
Add(serverEndpoint, new UdpClientState { Client = new UdpClient(localEndpoint) });
}
}
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
lock (this)
{
// we don't add the udp client because it already exists (listening to multiple servers from one socket)
Add(serverEndpoint, new UdpClientState());
}
}
return this[serverEndpoint];
}
public List<byte[]> GetServerLogData(IPEndPoint serverEndpoint)
{
try
{
var state = this[serverEndpoint];
if (state == null)
{
return new List<byte[]>();
}
// it's possible that we could be trying to read and write to the queue simultaneously so we need to wait
this[serverEndpoint].OnAction.Wait();
var data = new List<byte[]>();
while (this[serverEndpoint].AvailableLogData.Count > 0)
{
data.Add(this[serverEndpoint].AvailableLogData.Dequeue());
}
return data;
}
finally
{
if (this[serverEndpoint].OnAction.CurrentCount == 0)
{
this[serverEndpoint].OnAction.Release(1);
}
}
}
public void QueueServerLogData(IPEndPoint serverEndpoint, byte[] data)
{
var endpoint = Keys.FirstOrDefault(key =>
Equals(key.Address, serverEndpoint.Address) && key.Port == serverEndpoint.Port);
try
{
if (endpoint == null)
{
return;
}
// currently our expected start and end characters
var startsWithPrefix = StartsWith(data, "ÿÿÿÿprint\n");
var endsWithDelimiter = data[^1] == '\n';
// we have the data we expected
if (!startsWithPrefix || !endsWithDelimiter)
{
return;
}
// it's possible that we could be trying to read and write to the queue simultaneously so we need to wait
this[endpoint].OnAction.Wait();
this[endpoint].AvailableLogData.Enqueue(data);
}
finally
{
if (endpoint != null && this[endpoint].OnAction.CurrentCount == 0)
{
this[endpoint].OnAction.Release(1);
}
}
}
public bool EndPointExists(IPEndPoint serverEndpoint)
{
lock (this)
{
return ContainsKey(serverEndpoint);
}
}
private static bool StartsWith(byte[] sourceArray, string match)
{
if (sourceArray is null)
{
return false;
}
if (match.Length > sourceArray.Length)
{
return false;
}
return !match.Where((t, i) => sourceArray[i] != (byte)t).Any();
}
}
public class UdpClientState
{
public UdpClient Client { get; set; }
public Queue<byte[]> AvailableLogData { get; } = new();
public SemaphoreSlim OnAction { get; } = new(1, 1);
~UdpClientState()
{
OnAction.Dispose();
Client?.Dispose();
}
}

1726
Application/IW4MServer.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,54 @@
using IW4MAdmin.Application.API.Master;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Localization
{
public class Configure
public static class Configure
{
public static void Initialize(string customLocale)
public static ITranslationLookup Initialize(ILogger logger, IMasterApi apiInstance, ApplicationConfiguration applicationConfiguration)
{
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
string[] localizationFiles = Directory.GetFiles("Localization", $"*.{currentLocale}.json");
var useLocalTranslation = applicationConfiguration?.UseLocalTranslations ?? true;
var customLocale = applicationConfiguration?.EnableCustomLocale ?? false
? (applicationConfiguration.CustomLocale ?? "en-US")
: "en-US";
var currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
var localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
if (!useLocalTranslation)
{
try
{
var api = Endpoint.Get();
var localization = api.GetLocalization(currentLocale).Result;
var localization = apiInstance.GetLocalization(currentLocale).Result;
Utilities.CurrentLocalization = localization;
return;
return localization.LocalizationIndex;
}
catch (Exception)
catch (Exception ex)
{
// the online localization failed so will default to local files
logger.LogWarning(ex, "Could not download latest translations");
}
}
// culture doesn't exist so we just want language
if (localizationFiles.Length == 0)
{
localizationFiles = Directory.GetFiles("Localization", $"*.{currentLocale.Substring(0, 2)}*.json");
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale.Substring(0, 2)}*.json");
}
// language doesn't exist either so defaulting to english
if (localizationFiles.Length == 0)
{
localizationFiles = Directory.GetFiles("Localization", "*.en-US.json");
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), "*.en-US.json");
}
// this should never happen unless the localization folder is empty
@ -54,22 +63,26 @@ namespace IW4MAdmin.Application.Localization
{
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
if (eachLocalizationFile == null)
{
continue;
}
foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{
if (!localizationDict.TryAdd(item.Key, item.Value))
{
Program.ServerManager.GetLogger().WriteError($"Could not add locale string {item.Key} to localization");
logger.LogError("Could not add locale string {key} to localization", item.Key);
}
}
}
string localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{
LocalizationName = currentLocale,
};
return Utilities.CurrentLocalization.LocalizationIndex;
}
}
}

View File

@ -1,269 +0,0 @@
{
"LocalizationName": "en-US",
"LocalizationIndex": {
"Set": {
"BROADCAST_OFFLINE": "^5IW4MAdmin ^7is going ^1OFFLINE",
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
"COMMAND_HELP_OPTIONAL": "optional",
"COMMAND_HELP_SYNTAX": "syntax:",
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
"COMMAND_NOACCESS": "You do not have access to that command",
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
"COMMAND_TARGET_MULTI": "Multiple players match that name",
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
"COMMAND_UNKNOWN": "You entered an unknown command",
"COMMANDS_ADMINS_DESC": "list currently connected privileged clients",
"COMMANDS_ADMINS_NONE": "No visible administrators online",
"COMMANDS_ALIAS_ALIASES": "Aliases",
"COMMANDS_ALIAS_DESC": "get past aliases and ips of a client",
"COMMANDS_ALIAS_IPS": "IPs",
"COMMANDS_ARGS_CLEAR": "clear",
"COMMANDS_ARGS_CLIENTID": "client id",
"COMMANDS_ARGS_COMMANDS": "commands",
"COMMANDS_ARGS_DURATION": "duration (m|h|d|w|y)",
"COMMANDS_ARGS_INACTIVE": "inactive days",
"COMMANDS_ARGS_LEVEL": "level",
"COMMANDS_ARGS_MAP": "map",
"COMMANDS_ARGS_MESSAGE": "message",
"COMMANDS_ARGS_PASSWORD": "password",
"COMMANDS_ARGS_PLAYER": "player",
"COMMANDS_ARGS_REASON": "reason",
"COMMANDS_BAN_DESC": "permanently ban a client from the server",
"COMMANDS_BAN_FAIL": "You cannot ban",
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
"COMMANDS_BANINFO_DESC": "get information about a ban for a client",
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
"COMMANDS_FASTRESTART_DESC": "fast restart current map",
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
"COMMANDS_FIND_DESC": "find client in database",
"COMMANDS_FIND_EMPTY": "No players found",
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
"COMMANDS_FLAG_DESC": "flag a suspicious client and announce to admins on join",
"COMMANDS_FLAG_FAIL": "You cannot flag",
"COMMANDS_FLAG_SUCCESS": "You have flagged",
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
"COMMANDS_HELP_DESC": "list all available commands",
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
"COMMANDS_IP_DESC": "view your external IP address",
"COMMANDS_IP_SUCCESS": "Your external IP is",
"COMMANDS_KICK_DESC": "kick a client by name",
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
"COMMANDS_KICK_SUCCESS": "has been kicked",
"COMMANDS_LIST_DESC": "list active clients",
"COMMANDS_MAP_DESC": "change to specified map",
"COMMANDS_MAP_SUCCESS": "Changing to map",
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
"COMMANDS_MAPROTATE_DESC": "cycle to the next map in rotation",
"COMMANDS_MASK_DESC": "hide your presence as a privileged client",
"COMMANDS_MASK_OFF": "You are now unmasked",
"COMMANDS_MASK_ON": "You are now masked",
"COMMANDS_OWNER_DESC": "claim ownership of the server",
"COMMANDS_OWNER_FAIL": "This server already has an owner",
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
"COMMANDS_PING_DESC": "get client's latency",
"COMMANDS_PING_SELF": "Your latency is",
"COMMANDS_PING_TARGET": "latency is",
"COMMANDS_PLUGINS_DESC": "view all loaded plugins",
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
"COMMANDS_PM_DESC": "send message to other client",
"COMMANDS_PRUNE_DESC": "demote any privileged clients that have not connected recently (defaults to 30 days)",
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
"COMMANDS_QUIT_DESC": "quit IW4MAdmin",
"COMMANDS_RCON_DESC": "send rcon command to server",
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
"COMMANDS_REPORT_DESC": "report a client for suspicious behavior",
"COMMANDS_REPORT_FAIL": "You cannot report",
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
"COMMANDS_REPORTS_DESC": "get or clear recent reports",
"COMMANDS_REPORTS_NONE": "No players reported yet",
"COMMANDS_RULES_DESC": "list server rules",
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
"COMMANDS_SAY_DESC": "broadcast message to all clients",
"COMMANDS_SETLEVEL_DESC": "set client to specified privilege level",
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
"COMMANDS_SETPASSWORD_DESC": "set your authentication password",
"COMMANDS_TEMPBAN_DESC": "temporarily ban a client for specified time (defaults to 1 hour)",
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
"COMMANDS_UNBAN_DESC": "unban client by client id",
"COMMANDS_UNBAN_FAIL": "is not banned",
"COMMANDS_UNBAN_SUCCESS": "Successfully unbanned",
"COMMANDS_UPTIME_DESC": "get current application running time",
"COMMANDS_UPTIME_TEXT": "has been online for",
"COMMANDS_USAGE_DESC": "get application memory usage",
"COMMANDS_USAGE_TEXT": "is using",
"COMMANDS_WARN_DESC": "warn client for infringing rules",
"COMMANDS_WARN_FAIL": "You do not have the required privileges to warn",
"COMMANDS_WARNCLEAR_DESC": "remove all warnings for a client",
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
"COMMANDS_WHO_DESC": "give information about yourself",
"GLOBAL_DAYS": "days",
"GLOBAL_ERROR": "Error",
"GLOBAL_HOURS": "hours",
"GLOBAL_INFO": "Info",
"GLOBAL_MINUTES": "minutes",
"GLOBAL_REPORT": "If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command",
"GLOBAL_VERBOSE": "Verbose",
"GLOBAL_WARNING": "Warning",
"MANAGER_CONNECTION_REST": "Connection has been reestablished with",
"MANAGER_CONSOLE_NOSERV": "No servers are currently being monitored",
"MANAGER_EXIT": "Press any key to exit...",
"MANAGER_INIT_FAIL": "Fatal error during initialization",
"MANAGER_MONITORING_TEXT": "Now monitoring",
"MANAGER_SHUTDOWN_SUCCESS": "Shutdown complete",
"MANAGER_VERSION_CURRENT": "Your version is",
"MANAGER_VERSION_FAIL": "Could not get latest IW4MAdmin version",
"MANAGER_VERSION_SUCCESS": "IW4MAdmin is up to date",
"MANAGER_VERSION_UPDATE": "has an update. Latest version is",
"PLUGIN_IMPORTER_NOTFOUND": "No plugins found to load",
"PLUGIN_IMPORTER_REGISTERCMD": "Registered command",
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "login using password",
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "Your password is incorrect",
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "You are now logged in",
"PLUGINS_STATS_COMMANDS_RESET_DESC": "reset your stats to factory-new",
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "You must be connected to a server to reset your stats",
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Your stats for this server have been reset",
"PLUGINS_STATS_COMMANDS_TOP_DESC": "view the top 5 players in this server",
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Top Players",
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "view your stats",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "Cannot find the player you specified",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "The specified player must be ingame",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "You must be ingame to view your stats",
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Stats for",
"PLUGINS_STATS_TEXT_DEATHS": "DEATHS",
"PLUGINS_STATS_TEXT_KILLS": "KILLS",
"PLUGINS_STATS_TEXT_NOQUALIFY": "No players qualify for top stats yet",
"PLUGINS_STATS_TEXT_SKILL": "SKILL",
"SERVER_BAN_APPEAL": "appeal at",
"SERVER_BAN_PREV": "Previously banned for",
"SERVER_BAN_TEXT": "You're banned",
"SERVER_ERROR_ADDPLAYER": "Unable to add player",
"SERVER_ERROR_COMMAND_INGAME": "An internal error occured while processing your command",
"SERVER_ERROR_COMMAND_LOG": "command generated an error",
"SERVER_ERROR_COMMUNICATION": "Could not communicate with",
"SERVER_ERROR_DNE": "does not exist",
"SERVER_ERROR_DVAR": "Could not get the dvar value for",
"SERVER_ERROR_DVAR_HELP": "ensure the server has a map loaded",
"SERVER_ERROR_EXCEPTION": "Unexpected exception on",
"SERVER_ERROR_LOG": "Invalid game log file",
"SERVER_ERROR_PLUGIN": "An error occured loading plugin",
"SERVER_ERROR_POLLING": "reducing polling rate",
"SERVER_ERROR_UNFIXABLE": "Not monitoring server due to uncorrectable errors",
"SERVER_KICK_CONTROLCHARS": "Your name cannot contain control characters",
"SERVER_KICK_GENERICNAME": "Please change your name using /name",
"SERVER_KICK_MINNAME": "Your name must contain at least 3 characters",
"SERVER_KICK_NAME_INUSE": "Your name is being used by someone else",
"SERVER_KICK_TEXT": "You were kicked",
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs are not allowed on this server",
"SERVER_PLUGIN_ERROR": "A plugin generated an error",
"SERVER_REPORT_COUNT": "There are ^5{0} ^7recent reports",
"SERVER_TB_REMAIN": "You are temporarily banned",
"SERVER_TB_TEXT": "You're temporarily banned",
"SERVER_WARNING": "WARNING",
"SERVER_WARNLIMT_REACHED": "Too many warnings",
"SERVER_WEBSITE_GENERIC": "this server's website",
"SETUP_DISPLAY_SOCIAL": "Display social media link on webfront (discord, website, VK, etc..)",
"SETUP_ENABLE_CUSTOMSAY": "Enable custom say name",
"SETUP_ENABLE_MULTIOWN": "Enable multiple owners",
"SETUP_ENABLE_STEPPEDPRIV": "Enable stepped privilege hierarchy",
"SETUP_ENABLE_VPNS": "Enable client VPNs",
"SETUP_ENABLE_WEBFRONT": "Enable webfront",
"SETUP_ENCODING_STRING": "Enter encoding string",
"SETUP_IPHUB_KEY": "Enter iphub.info api key",
"SETUP_SAY_NAME": "Enter custom say name",
"SETUP_SERVER_IP": "Enter server IP Address",
"SETUP_SERVER_MANUALLOG": "Enter manual log file path",
"SETUP_SERVER_PORT": "Enter server port",
"SETUP_SERVER_RCON": "Enter server RCon password",
"SETUP_SERVER_SAVE": "Configuration saved, add another",
"SETUP_SERVER_USEIW5M": "Use Pluto IW5 Parser",
"SETUP_SERVER_USET6M": "Use Pluto T6 parser",
"SETUP_SOCIAL_LINK": "Enter social media link",
"SETUP_SOCIAL_TITLE": "Enter social media name",
"SETUP_USE_CUSTOMENCODING": "Use custom encoding parser",
"WEBFRONT_ACTION_BAN_NAME": "Ban",
"WEBFRONT_ACTION_LABEL_ID": "Client ID",
"WEBFRONT_ACTION_LABEL_PASSWORD": "Password",
"WEBFRONT_ACTION_LABEL_REASON": "Reason",
"WEBFRONT_ACTION_LOGIN_NAME": "Login",
"WEBFRONT_ACTION_UNBAN_NAME": "Unban",
"WEBFRONT_CLIENT_META_FALSE": "Is not",
"WEBFRONT_CLIENT_META_JOINED": "Joined with alias",
"WEBFRONT_CLIENT_META_MASKED": "Masked",
"WEBFRONT_CLIENT_META_TRUE": "Is",
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Privileged Clients",
"WEBFRONT_CLIENT_PROFILE_TITLE": "Profile",
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Clients Matching",
"WEBFRONT_CONSOLE_EXECUTE": "Execute",
"WEBFRONT_CONSOLE_TITLE": "Web Console",
"WEBFRONT_ERROR_DESC": "IW4MAdmin encountered an error",
"WEBFRONT_ERROR_GENERIC_DESC": "An error occurred while processing your request",
"WEBFRONT_ERROR_GENERIC_TITLE": "Sorry!",
"WEBFRONT_ERROR_TITLE": "Error!",
"WEBFRONT_HOME_TITLE": "Server Overview",
"WEBFRONT_NAV_CONSOLE": "Console",
"WEBFRONT_NAV_DISCORD": "Discord",
"WEBFRONT_NAV_HOME": "Home",
"WEBFRONT_NAV_LOGOUT": "Logout",
"WEBFRONT_NAV_PENALTIES": "Penalties",
"WEBFRONT_NAV_PRIVILEGED": "Admins",
"WEBFRONT_NAV_PROFILE": "Client Profile",
"WEBFRONT_NAV_SEARCH": "Find Client",
"WEBFRONT_NAV_SOCIAL": "Social",
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Admin",
"WEBFRONT_PENALTY_TEMPLATE_AGO": "ago",
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Name",
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Offense",
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "left",
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Show",
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Show only",
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Time/Left",
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Type",
"WEBFRONT_PENALTY_TITLE": "Client Penalties",
"WEBFRONT_PROFILE_FSEEN": "First seen",
"WEBFRONT_PROFILE_LEVEL": "Level",
"WEBFRONT_PROFILE_LSEEN": "Last seen",
"WEBFRONT_PROFILE_PLAYER": "Played",
"PLUGIN_STATS_SETUP_ENABLEAC": "Enable server-side anti-cheat (IW4 only)",
"PLUGIN_STATS_ERROR_ADD": "Could not add server to server stats",
"PLUGIN_STATS_CHEAT_DETECTED": "You appear to be cheating",
"PLUGINS_STATS_TEXT_KDR": "KDR",
"PLUGINS_STATS_META_SPM": "Score per Minute",
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7hails from ^5{{ClientLocation}}",
"PLUGINS_WELCOME_USERWELCOME": "Welcome ^5{{ClientName}}^7, this is your ^5{{TimesConnected}} ^7time connecting!",
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} has joined the server",
"PLUGINS_LOGIN_AUTH": "not logged in",
"PLUGINS_PROFANITY_SETUP_ENABLE": "Enable profanity deterring",
"PLUGINS_PROFANITY_WARNMSG": "Please do not use profanity on this server",
"PLUGINS_PROFANITY_KICKMSG": "Excessive use of profanity",
"GLOBAL_DEBUG": "Debug",
"COMMANDS_UNFLAG_DESC": "Remove flag for client",
"COMMANDS_UNFLAG_FAIL": "You cannot unflag",
"COMMANDS_UNFLAG_NOTFLAGGED": "Client is not flagged",
"COMMANDS_FLAG_ALREADYFLAGGED": "Client is already flagged",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Most Played",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "view the top 5 dedicated players on the server",
"WEBFRONT_PROFILE_MESSAGES": "Messages",
"WEBFRONT_CLIENT_META_CONNECTIONS": "Connections",
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Rating",
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Performance"
}
}
}

View File

@ -1,269 +0,0 @@
{
"LocalizationName": "es-EC",
"LocalizationIndex": {
"Set": {
"BROADCAST_OFFLINE": "^5IW4MAdmin ^7está ^1DESCONECTANDOSE",
"BROADCAST_ONLINE": "^5IW4MADMIN ^7está ahora ^2en línea",
"COMMAND_HELP_OPTIONAL": "opcional",
"COMMAND_HELP_SYNTAX": "sintaxis:",
"COMMAND_MISSINGARGS": "No se han proporcionado suficientes argumentos",
"COMMAND_NOACCESS": "Tú no tienes acceso a ese comando",
"COMMAND_NOTAUTHORIZED": "Tú no estás autorizado para ejecutar ese comando",
"COMMAND_TARGET_MULTI": "Múltiples jugadores coinciden con ese nombre",
"COMMAND_TARGET_NOTFOUND": "No se puede encontrar el jugador especificado",
"COMMAND_UNKNOWN": "Has ingresado un comando desconocido",
"COMMANDS_ADMINS_DESC": "enlistar clientes privilegiados actualmente conectados",
"COMMANDS_ADMINS_NONE": "No hay administradores visibles en línea",
"COMMANDS_ALIAS_ALIASES": "Aliases",
"COMMANDS_ALIAS_DESC": "obtener alias e ips anteriores de un cliente",
"COMMANDS_ALIAS_IPS": "IPs",
"COMMANDS_ARGS_CLEAR": "borrar",
"COMMANDS_ARGS_CLIENTID": "id del cliente",
"COMMANDS_ARGS_COMMANDS": "comandos",
"COMMANDS_ARGS_DURATION": "duración (m|h|d|w|y)",
"COMMANDS_ARGS_INACTIVE": "días inactivo",
"COMMANDS_ARGS_LEVEL": "nivel",
"COMMANDS_ARGS_MAP": "mapa",
"COMMANDS_ARGS_MESSAGE": "mensaje",
"COMMANDS_ARGS_PASSWORD": "contraseña",
"COMMANDS_ARGS_PLAYER": "jugador",
"COMMANDS_ARGS_REASON": "razón",
"COMMANDS_BAN_DESC": "banear permanentemente un cliente del servidor",
"COMMANDS_BAN_FAIL": "Tú no puedes banear",
"COMMANDS_BAN_SUCCESS": "ha sido baneado permanentemente",
"COMMANDS_BANINFO_DESC": "obtener información sobre el ban de un cliente",
"COMMANDS_BANINFO_NONE": "No se encontró ban activo para ese jugador",
"COMMANDS_BANINO_SUCCESS": "fue baneado por ^5{0} ^7debido a:",
"COMMANDS_FASTRESTART_DESC": "dar reinicio rápido al mapa actial",
"COMMANDS_FASTRESTART_MASKED": "Al mapa se le ha dado un reinicio rápido",
"COMMANDS_FASTRESTART_UNMASKED": "ha dado rápido reinicio al mapa",
"COMMANDS_FIND_DESC": "encontrar cliente en la base de datos",
"COMMANDS_FIND_EMPTY": "No se encontraron jugadores",
"COMMANDS_FIND_MIN": "Por Favor introduzca al menos 3 caracteres",
"COMMANDS_FLAG_DESC": "marcar un cliente sospechoso y anunciar a los administradores al unirse",
"COMMANDS_FLAG_FAIL": "Tú no puedes marcar",
"COMMANDS_FLAG_SUCCESS": "Has marcado a",
"COMMANDS_FLAG_UNFLAG": "Has desmarcado a",
"COMMANDS_HELP_DESC": "enlistar todos los comandos disponibles",
"COMMANDS_HELP_MOREINFO": "Escribe !help <nombre del comando> para obtener la sintaxis de uso del comando",
"COMMANDS_HELP_NOTFOUND": "No se ha podido encontrar ese comando",
"COMMANDS_IP_DESC": "ver tu dirección IP externa",
"COMMANDS_IP_SUCCESS": "Tu IP externa es",
"COMMANDS_KICK_DESC": "expulsar a un cliente por su nombre",
"COMMANDS_KICK_FAIL": "No tienes los privilegios necesarios para expulsar a",
"COMMANDS_KICK_SUCCESS": "ha sido expulsado",
"COMMANDS_LIST_DESC": "enlistar clientes activos",
"COMMANDS_MAP_DESC": "cambiar al mapa especificado",
"COMMANDS_MAP_SUCCESS": "Cambiando al mapa",
"COMMANDS_MAP_UKN": "Intentando cambiar a un mapa desconocido",
"COMMANDS_MAPROTATE": "Rotación de mapa en ^55 ^7segundos",
"COMMANDS_MAPROTATE_DESC": "pasar al siguiente mapa en rotación",
"COMMANDS_MASK_DESC": "esconde tu presencia como un cliente privilegiado",
"COMMANDS_MASK_OFF": "Ahora estás desenmascarado",
"COMMANDS_MASK_ON": "Ahora estás enmascarado",
"COMMANDS_OWNER_DESC": "reclamar la propiedad del servidor",
"COMMANDS_OWNER_FAIL": "Este servidor ya tiene un propietario",
"COMMANDS_OWNER_SUCCESS": "¡Felicidades, has reclamado la propiedad de este servidor!",
"COMMANDS_PASSWORD_FAIL": "Tu contraseña debe tener al menos 5 caracteres de largo",
"COMMANDS_PASSWORD_SUCCESS": "Su contraseña ha sido establecida con éxito",
"COMMANDS_PING_DESC": "obtener ping del cliente",
"COMMANDS_PING_SELF": "Tu ping es",
"COMMANDS_PING_TARGET": "ping es",
"COMMANDS_PLUGINS_DESC": "ver todos los complementos cargados",
"COMMANDS_PLUGINS_LOADED": "Complementos cargados",
"COMMANDS_PM_DESC": "enviar mensaje a otro cliente",
"COMMANDS_PRUNE_DESC": "degradar a los clientes con privilegios que no se hayan conectado recientemente (el valor predeterminado es 30 días)",
"COMMANDS_PRUNE_FAIL": "Número inválido de días inactivos",
"COMMANDS_PRUNE_SUCCESS": "los usuarios privilegiados inactivos fueron podados",
"COMMANDS_QUIT_DESC": "salir de IW4MAdmin",
"COMMANDS_RCON_DESC": "enviar el comando rcon al servidor",
"COMMANDS_RCON_SUCCESS": "Exitosamente enviado el comando RCon",
"COMMANDS_REPORT_DESC": "reportar un cliente por comportamiento sospechoso",
"COMMANDS_REPORT_FAIL": "Tú no puedes reportar",
"COMMANDS_REPORT_FAIL_CAMP": "No puedes reportar a un jugador por campear",
"COMMANDS_REPORT_FAIL_DUPLICATE": "Ya has reportado a este jugador",
"COMMANDS_REPORT_FAIL_SELF": "No puedes reportarte a ti mismo",
"COMMANDS_REPORT_SUCCESS": "Gracias por su reporte, un administrador ha sido notificado",
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reportes borrados con éxito",
"COMMANDS_REPORTS_DESC": "obtener o borrar informes recientes",
"COMMANDS_REPORTS_NONE": "No hay jugadores reportados aun",
"COMMANDS_RULES_DESC": "enlistar reglas del servidor",
"COMMANDS_RULES_NONE": "El propietario del servidor no ha establecido ninguna regla",
"COMMANDS_SAY_DESC": "transmitir el mensaje a todos los clientes",
"COMMANDS_SETLEVEL_DESC": "establecer el cliente al nivel de privilegio especificado",
"COMMANDS_SETLEVEL_FAIL": "Grupo inválido especificado",
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "Tú solo puedes promover ^5{0} ^7a ^5{1} ^7o menor privilegio",
"COMMANDS_SETLEVEL_OWNER": "Solo puede haber un propietario. Modifica tu configuración si múltiples propietarios son requeridos",
"COMMANDS_SETLEVEL_SELF": "No puedes cambiar tu propio nivel",
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "Este servidor no te permite promover",
"COMMANDS_SETLEVEL_SUCCESS": "fue promovido con éxito",
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "¡Felicitaciones! has ha sido promovido a",
"COMMANDS_SETPASSWORD_DESC": "configura tu contraseña de autenticación",
"COMMANDS_TEMPBAN_DESC": "banear temporalmente a un cliente por el tiempo especificado (predeterminado en 1 hora)",
"COMMANDS_TEMPBAN_FAIL": "Tú no puedes banear temporalmente",
"COMMANDS_TEMPBAN_SUCCESS": "ha sido baneado temporalmente por",
"COMMANDS_UNBAN_DESC": "desbanear al cliente por ID",
"COMMANDS_UNBAN_FAIL": "no está baneado",
"COMMANDS_UNBAN_SUCCESS": "Exitosamente desbaneado",
"COMMANDS_UPTIME_DESC": "obtener el tiempo de ejecución de la aplicación actual",
"COMMANDS_UPTIME_TEXT": "ha estado en línea por",
"COMMANDS_USAGE_DESC": "obtener uso de la memoria de la aplicación",
"COMMANDS_USAGE_TEXT": "está usando",
"COMMANDS_WARN_DESC": "advertir al cliente por infringir las reglas",
"COMMANDS_WARN_FAIL": "No tiene los privilegios necesarios para advertir a",
"COMMANDS_WARNCLEAR_DESC": "eliminar todas las advertencias de un cliente",
"COMMANDS_WARNCLEAR_SUCCESS": "Todas las advertencias borradas para",
"COMMANDS_WHO_DESC": "da información sobre ti",
"GLOBAL_DAYS": "días",
"GLOBAL_ERROR": "Error",
"GLOBAL_HOURS": "horas",
"GLOBAL_INFO": "Información",
"GLOBAL_MINUTES": "minutos",
"GLOBAL_REPORT": "Si sospechas que alguien ^5usa cheats ^7usa el comando ^5!report",
"GLOBAL_VERBOSE": "Detallado",
"GLOBAL_WARNING": "Advertencia",
"MANAGER_CONNECTION_REST": "La conexión ha sido restablecida con",
"MANAGER_CONSOLE_NOSERV": "No hay servidores que estén siendo monitoreados en este momento",
"MANAGER_EXIT": "Presione cualquier tecla para salir...",
"MANAGER_INIT_FAIL": "Error fatal durante la inicialización",
"MANAGER_MONITORING_TEXT": "Ahora monitoreando",
"MANAGER_SHUTDOWN_SUCCESS": "Apagado completo",
"MANAGER_VERSION_CURRENT": "Tu versión es",
"MANAGER_VERSION_FAIL": "No se ha podido conseguir la última versión de IW4MAdmin",
"MANAGER_VERSION_SUCCESS": "IW4MAdmin está actualizado",
"MANAGER_VERSION_UPDATE": "tiene una actualización. La última versión es",
"PLUGIN_IMPORTER_NOTFOUND": "No se encontraron complementos para cargar",
"PLUGIN_IMPORTER_REGISTERCMD": "Comando registrado",
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "iniciar sesión usando la contraseña",
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "tu contraseña es incorrecta",
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "Ahora está conectado",
"PLUGINS_STATS_COMMANDS_RESET_DESC": "restablece tus estadísticas a las nuevas de fábrica",
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "Debes estar conectado a un servidor para restablecer tus estadísticas",
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Tus estadísticas para este servidor se han restablecido",
"PLUGINS_STATS_COMMANDS_TOP_DESC": "ver los 5 mejores jugadores en este servidor",
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Mejores Jugadores",
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "ver tus estadísticas",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "No se puede encontrar el jugador que especificó",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "El jugador especificado debe estar dentro del juego",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "Debes estar dentro del juego para ver tus estadísticas",
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Estadísticas para",
"PLUGINS_STATS_TEXT_DEATHS": "Muertes",
"PLUGINS_STATS_TEXT_KILLS": "Asesinatos",
"PLUGINS_STATS_TEXT_NOQUALIFY": "No hay jugadores que califiquen para los primeros lugares aun",
"PLUGINS_STATS_TEXT_SKILL": "Habilidad",
"SERVER_BAN_APPEAL": "apela en",
"SERVER_BAN_PREV": "Baneado anteriormente por",
"SERVER_BAN_TEXT": "Estás baneado",
"SERVER_ERROR_ADDPLAYER": "Incapaz de añadir al jugador",
"SERVER_ERROR_COMMAND_INGAME": "Un error interno ocurrió mientras se procesaba tu comando",
"SERVER_ERROR_COMMAND_LOG": "Comando generó error",
"SERVER_ERROR_COMMUNICATION": "No se ha podido comunicar con",
"SERVER_ERROR_DNE": "No existe",
"SERVER_ERROR_DVAR": "No se pudo obtener el valor dvar",
"SERVER_ERROR_DVAR_HELP": "asegúrate de que el servidor tenga un mapa cargado",
"SERVER_ERROR_EXCEPTION": "Excepción inesperada en",
"SERVER_ERROR_LOG": "Archivo de registro del juego invalido",
"SERVER_ERROR_PLUGIN": "Un error ocurrió mientras se cargaba el complemente",
"SERVER_ERROR_POLLING": "reduciendo la tasa de sondeo",
"SERVER_ERROR_UNFIXABLE": "No se está supervisando el servidor debido a errores incorregibles",
"SERVER_KICK_CONTROLCHARS": "Tu nombre no puede contener caracteres de control",
"SERVER_KICK_GENERICNAME": "Por favor cambia tu nombre usando /name",
"SERVER_KICK_MINNAME": "Tu nombre debe contener al menos 3 caracteres",
"SERVER_KICK_NAME_INUSE": "Tu nombre está siendo usado por alguien más",
"SERVER_KICK_TEXT": "Fuiste expulsado",
"SERVER_KICK_VPNS_NOTALLOWED": "Las VPNs no están permitidas en este servidor",
"SERVER_PLUGIN_ERROR": "Un complemento generó un error",
"SERVER_REPORT_COUNT": "Hay ^5{0} ^7reportes recientes",
"SERVER_TB_REMAIN": "Tú estás temporalmente baneado",
"SERVER_TB_TEXT": "Estás temporalmente baneado",
"SERVER_WARNING": "ADVERTENCIA",
"SERVER_WARNLIMT_REACHED": "Muchas advertencias",
"SERVER_WEBSITE_GENERIC": "el sitio web de este servidor",
"SETUP_DISPLAY_SOCIAL": "Mostrar el link del medio de comunicación en la parte frontal de la web. (discord, website, VK, etc..)",
"SETUP_ENABLE_CUSTOMSAY": "Habilitar nombre a decir personalizado",
"SETUP_ENABLE_MULTIOWN": "Habilitar múltiples propietarios",
"SETUP_ENABLE_STEPPEDPRIV": "Habilitar jerarquía de privilegios por escalones",
"SETUP_ENABLE_VPNS": "Habilitar VPNs clientes",
"SETUP_ENABLE_WEBFRONT": "Habilitar frente de la web",
"SETUP_ENCODING_STRING": "Ingresar cadena de codificación",
"SETUP_IPHUB_KEY": "Ingresar clave api de iphub.info",
"SETUP_SAY_NAME": "Ingresar nombre a decir personalizado",
"SETUP_SERVER_IP": "Ingresar Dirección IP del servidor",
"SETUP_SERVER_MANUALLOG": "Ingresar manualmente la ruta del archivo de registro",
"SETUP_SERVER_PORT": "Ingresar puerto del servidor",
"SETUP_SERVER_RCON": "Ingresar contraseña RCon del servidor",
"SETUP_SERVER_SAVE": "Configuración guardada, añadir otra",
"SETUP_SERVER_USEIW5M": "Usar analizador Pluto IW5",
"SETUP_SERVER_USET6M": "Usar analizador Pluto T6",
"SETUP_SOCIAL_LINK": "Ingresar link del medio de comunicación",
"SETUP_SOCIAL_TITLE": "Ingresa el nombre de la red de comunicación",
"SETUP_USE_CUSTOMENCODING": "Usar analizador de codificación personalizado",
"WEBFRONT_ACTION_BAN_NAME": "Ban",
"WEBFRONT_ACTION_LABEL_ID": "ID del Cliente",
"WEBFRONT_ACTION_LABEL_PASSWORD": "Contraseña",
"WEBFRONT_ACTION_LABEL_REASON": "Razón",
"WEBFRONT_ACTION_LOGIN_NAME": "Inicio de sesión",
"WEBFRONT_ACTION_UNBAN_NAME": "Desban",
"WEBFRONT_CLIENT_META_FALSE": "No está",
"WEBFRONT_CLIENT_META_JOINED": "Se unió con el alias",
"WEBFRONT_CLIENT_META_MASKED": "Enmascarado",
"WEBFRONT_CLIENT_META_TRUE": "Está",
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Clientes privilegiados",
"WEBFRONT_CLIENT_PROFILE_TITLE": "Perfil",
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Clientes que concuerdan",
"WEBFRONT_CONSOLE_EXECUTE": "Ejecutar",
"WEBFRONT_CONSOLE_TITLE": "Consola Web",
"WEBFRONT_ERROR_DESC": "IW4MAdmin encontró un error",
"WEBFRONT_ERROR_GENERIC_DESC": "Un error ha ocurrido mientras se procesaba tu solicitud",
"WEBFRONT_ERROR_GENERIC_TITLE": "¡Lo lamento!",
"WEBFRONT_ERROR_TITLE": "¡Error!",
"WEBFRONT_HOME_TITLE": "Vista general del servidor",
"WEBFRONT_NAV_CONSOLE": "Consola",
"WEBFRONT_NAV_DISCORD": "Discord",
"WEBFRONT_NAV_HOME": "Inicio",
"WEBFRONT_NAV_LOGOUT": "Cerrar sesión",
"WEBFRONT_NAV_PENALTIES": "Sanciones",
"WEBFRONT_NAV_PRIVILEGED": "Administradores",
"WEBFRONT_NAV_PROFILE": "Perfil del cliente",
"WEBFRONT_NAV_SEARCH": "Encontrar cliente",
"WEBFRONT_NAV_SOCIAL": "Social",
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Administrador",
"WEBFRONT_PENALTY_TEMPLATE_AGO": "atrás",
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Nombre",
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Ofensa",
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "restante",
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Mostrar",
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Mostrar solamente",
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Tiempo/Restante",
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Tipo",
"WEBFRONT_PENALTY_TITLE": "Faltas del cliente",
"WEBFRONT_PROFILE_FSEEN": "Primera vez visto hace",
"WEBFRONT_PROFILE_LEVEL": "Nivel",
"WEBFRONT_PROFILE_LSEEN": "Última vez visto hace",
"WEBFRONT_PROFILE_PLAYER": "Jugadas",
"PLUGIN_STATS_SETUP_ENABLEAC": "Habilitar anti-trampas junto al servidor (solo IW4)",
"PLUGIN_STATS_ERROR_ADD": "No se puedo añadir servidor a los estados del servidor",
"PLUGIN_STATS_CHEAT_DETECTED": "Pareces estar haciendo trampa",
"PLUGINS_STATS_TEXT_KDR": "KDR",
"PLUGINS_STATS_META_SPM": "Puntaje por minuto",
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7llega desde ^5{{ClientLocation}}",
"PLUGINS_WELCOME_USERWELCOME": "¡Bienvenido ^5{{ClientName}}^7, esta es tu visita numero ^5{{TimesConnected}} ^7 en el servidor!",
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} Se ha unido al servidor",
"PLUGINS_LOGIN_AUTH": "No registrado",
"PLUGINS_PROFANITY_SETUP_ENABLE": "Habilitar la disuasión de blasfemias",
"PLUGINS_PROFANITY_WARNMSG": "Por favor no uses blasfemias en este servidor",
"PLUGINS_PROFANITY_KICKMSG": "Excesivo uso de blasfemias",
"GLOBAL_DEBUG": "Depurar",
"COMMANDS_UNFLAG_DESC": "Remover marca del cliente",
"COMMANDS_UNFLAG_FAIL": "Tu no puedes desmarcar",
"COMMANDS_UNFLAG_NOTFLAGGED": "El cliente no está marcado",
"COMMANDS_FLAG_ALREADYFLAGGED": "El cliente yá se encuentra marcado",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Más jugado",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "ver el Top 5 de jugadores dedicados en el servidor",
"WEBFRONT_PROFILE_MESSAGES": "Mensajes",
"WEBFRONT_CLIENT_META_CONNECTIONS": "Conexiones",
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Clasificación",
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Desempeño"
}
}
}

View File

@ -1,269 +0,0 @@
{
"LocalizationName": "pt-BR",
"LocalizationIndex": {
"Set": {
"BROADCAST_OFFLINE": "IW4MAdmin ficou offline",
"BROADCAST_ONLINE": "^5IW4MADMIN ^7agora está ^2ONLINE",
"COMMAND_HELP_OPTIONAL": "opcional",
"COMMAND_HELP_SYNTAX": "sintaxe:",
"COMMAND_MISSINGARGS": "Não foram oferecidos argumentos suficientes",
"COMMAND_NOACCESS": "Você não tem acesso a este comando",
"COMMAND_NOTAUTHORIZED": "Você não está autorizado a executar este comando",
"COMMAND_TARGET_MULTI": "Vários jogadores correspondem a esse nome",
"COMMAND_TARGET_NOTFOUND": "Não é possível encontrar o jogador especificado",
"COMMAND_UNKNOWN": "Você digitou um comando desconhecido",
"COMMANDS_ADMINS_DESC": "lista os clientes privilegiados conectados no momento",
"COMMANDS_ADMINS_NONE": "Não há administradores visíveis online",
"COMMANDS_ALIAS_ALIASES": "Nomes registrados",
"COMMANDS_ALIAS_DESC": "obtém a lista de histórico de nomes que o jogador usou no servidor",
"COMMANDS_ALIAS_IPS": "IPs",
"COMMANDS_ARGS_CLEAR": "apagar",
"COMMANDS_ARGS_CLIENTID": "id do jogador",
"COMMANDS_ARGS_COMMANDS": "comandos",
"COMMANDS_ARGS_DURATION": "duração (m|h|d|w|y)",
"COMMANDS_ARGS_INACTIVE": "dias inativos",
"COMMANDS_ARGS_LEVEL": "nível",
"COMMANDS_ARGS_MAP": "mapa",
"COMMANDS_ARGS_MESSAGE": "mensagem",
"COMMANDS_ARGS_PASSWORD": "senha",
"COMMANDS_ARGS_PLAYER": "jogador",
"COMMANDS_ARGS_REASON": "razão",
"COMMANDS_BAN_DESC": "banir permanentemente um cliente do servidor",
"COMMANDS_BAN_FAIL": "Você não pode banir permanentemente",
"COMMANDS_BAN_SUCCESS": "foi banido permanentemente",
"COMMANDS_BANINFO_DESC": "obtém informações sobre um banimento para um jogador",
"COMMANDS_BANINFO_NONE": "Nenhum banimento ativo foi encontrado para esse jogador",
"COMMANDS_BANINO_SUCCESS": "foi banido por ^5{0} ^7por:",
"COMMANDS_FASTRESTART_DESC": "reinicializa rapidamente o mapa atual, não recomendável o uso várias vezes seguidas",
"COMMANDS_FASTRESTART_MASKED": "O mapa foi reiniciado rapidamente",
"COMMANDS_FASTRESTART_UNMASKED": "reiniciou rapidamente o mapa",
"COMMANDS_FIND_DESC": "acha o jogador na base de dados",
"COMMANDS_FIND_EMPTY": "Nenhum jogador foi encontrado",
"COMMANDS_FIND_MIN": "Por favor, insira pelo menos 3 caracteres",
"COMMANDS_FLAG_DESC": "sinaliza um cliente suspeito e anuncia aos administradores ao entrar no servidor",
"COMMANDS_FLAG_FAIL": "Você não pode sinalizar",
"COMMANDS_FLAG_SUCCESS": "Você sinalizou",
"COMMANDS_FLAG_UNFLAG": "Você tirou a sinalização de",
"COMMANDS_HELP_DESC": "lista todos os comandos disponíveis",
"COMMANDS_HELP_MOREINFO": "Digite !help <comando> para saber como usar o comando",
"COMMANDS_HELP_NOTFOUND": "Não foi possível encontrar esse comando",
"COMMANDS_IP_DESC": "mostrar o seu endereço IP externo",
"COMMANDS_IP_SUCCESS": "Seu endereço IP externo é",
"COMMANDS_KICK_DESC": "expulsa o jogador pelo nome",
"COMMANDS_KICK_FAIL": "Você não tem os privilégios necessários para expulsar",
"COMMANDS_KICK_SUCCESS": "foi expulso",
"COMMANDS_LIST_DESC": "lista os jogadores ativos na partida",
"COMMANDS_MAP_DESC": "muda para o mapa especificado",
"COMMANDS_MAP_SUCCESS": "Mudando o mapa para",
"COMMANDS_MAP_UKN": "Tentando mudar para o mapa desconhecido",
"COMMANDS_MAPROTATE": "Rotacionando o mapa em ^55 ^7segundos",
"COMMANDS_MAPROTATE_DESC": "avança para o próximo mapa da rotação",
"COMMANDS_MASK_DESC": "esconde a sua presença como um jogador privilegiado",
"COMMANDS_MASK_OFF": "Você foi desmascarado",
"COMMANDS_MASK_ON": "Você agora está mascarado",
"COMMANDS_OWNER_DESC": "reivindica a propriedade do servidor",
"COMMANDS_OWNER_FAIL": "Este servidor já tem um dono",
"COMMANDS_OWNER_SUCCESS": "Parabéns, você reivindicou a propriedade deste servidor!",
"COMMANDS_PASSWORD_FAIL": "Sua senha deve ter pelo menos 5 caracteres",
"COMMANDS_PASSWORD_SUCCESS": "Sua senha foi configurada com sucesso",
"COMMANDS_PING_DESC": "mostra o quanto de latência tem o jogador",
"COMMANDS_PING_SELF": "Sua latência é",
"COMMANDS_PING_TARGET": "latência é",
"COMMANDS_PLUGINS_DESC": "mostra todos os plugins que estão carregados",
"COMMANDS_PLUGINS_LOADED": "Plugins carregados",
"COMMANDS_PM_DESC": "envia a mensagem para o outro jogador de maneira privada, use /!pm para ter efeito, se possível",
"COMMANDS_PRUNE_DESC": "rebaixa qualquer jogador privilegiado que não tenha se conectado recentemente (o padrão é 30 dias)",
"COMMANDS_PRUNE_FAIL": "Número inválido de dias ativo",
"COMMANDS_PRUNE_SUCCESS": "usuários privilegiados inativos foram removidos",
"COMMANDS_QUIT_DESC": "sair do IW4MAdmin",
"COMMANDS_RCON_DESC": "envia o comando Rcon para o servidor",
"COMMANDS_RCON_SUCCESS": "O comando para o RCon foi enviado com sucesso!",
"COMMANDS_REPORT_DESC": "denuncia o jogador por comportamento suspeito",
"COMMANDS_REPORT_FAIL": "Você não pode reportar",
"COMMANDS_REPORT_FAIL_CAMP": "Você não pode denunciar o jogador por camperar",
"COMMANDS_REPORT_FAIL_DUPLICATE": "Você já denunciou o jogador",
"COMMANDS_REPORT_FAIL_SELF": "Você não pode reportar a si mesmo",
"COMMANDS_REPORT_SUCCESS": "Obrigado pela sua denúncia, um administrador foi notificado",
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Lista de denúncias limpa com sucesso",
"COMMANDS_REPORTS_DESC": "obtém ou limpa as denúncias recentes",
"COMMANDS_REPORTS_NONE": "Ninguém foi denunciado ainda",
"COMMANDS_RULES_DESC": "lista as regras do servidor",
"COMMANDS_RULES_NONE": "O proprietário do servidor não definiu nenhuma regra, sinta-se livre",
"COMMANDS_SAY_DESC": "transmite mensagem para todos os jogadores",
"COMMANDS_SETLEVEL_DESC": "define o jogador para o nível de privilégio especificado",
"COMMANDS_SETLEVEL_FAIL": "grupo especificado inválido",
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "Você só pode promover do ^5{0} ^7para ^5{1} ^7ou um nível menor",
"COMMANDS_SETLEVEL_OWNER": "Só pode haver 1 dono. Modifique suas configurações se vários proprietários forem necessários",
"COMMANDS_SETLEVEL_SELF": "Você não pode mudar seu próprio nível",
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "Este servidor não permite que você promova",
"COMMANDS_SETLEVEL_SUCCESS": "foi promovido com sucesso",
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Parabéns! Você foi promovido para",
"COMMANDS_SETPASSWORD_DESC": "define sua senha de autenticação",
"COMMANDS_TEMPBAN_DESC": "bane temporariamente um jogador por tempo especificado (o padrão é 1 hora)",
"COMMANDS_TEMPBAN_FAIL": "Você não pode banir temporariamente",
"COMMANDS_TEMPBAN_SUCCESS": "foi banido temporariamente por",
"COMMANDS_UNBAN_DESC": "retira o banimento de um jogador pelo seu ID",
"COMMANDS_UNBAN_FAIL": "não está banido",
"COMMANDS_UNBAN_SUCCESS": "Foi retirado o banimento com sucesso",
"COMMANDS_UPTIME_DESC": "obtém o tempo de execução do aplicativo a quando aberto",
"COMMANDS_UPTIME_TEXT": "está online por",
"COMMANDS_USAGE_DESC": "vê quanto o aplicativo está usando de memória RAM do seu computador",
"COMMANDS_USAGE_TEXT": "está usando",
"COMMANDS_WARN_DESC": "adverte o cliente por infringir as regras",
"COMMANDS_WARN_FAIL": "Você não tem os privilégios necessários para advertir",
"COMMANDS_WARNCLEAR_DESC": "remove todos os avisos para um cliente",
"COMMANDS_WARNCLEAR_SUCCESS": "Todos as advertências foram apagados para",
"COMMANDS_WHO_DESC": "dá informações sobre você",
"GLOBAL_DAYS": "dias",
"GLOBAL_ERROR": "Erro",
"GLOBAL_HOURS": "horas",
"GLOBAL_INFO": "Informação",
"GLOBAL_MINUTES": "minutos",
"GLOBAL_REPORT": "Se você está suspeitando alguém de alguma ^5TRAPAÇA ^7use o comando ^5!report",
"GLOBAL_VERBOSE": "Detalhe",
"GLOBAL_WARNING": "AVISO",
"MANAGER_CONNECTION_REST": "A conexão foi reestabelecida com",
"MANAGER_CONSOLE_NOSERV": "Não há servidores sendo monitorados neste momento",
"MANAGER_EXIT": "Pressione qualquer tecla para sair...",
"MANAGER_INIT_FAIL": "Erro fatal durante a inicialização",
"MANAGER_MONITORING_TEXT": "Agora monitorando",
"MANAGER_SHUTDOWN_SUCCESS": "Desligamento concluído",
"MANAGER_VERSION_CURRENT": "Está é a sua versão",
"MANAGER_VERSION_FAIL": "Não foi possível obter a versão mais recente do IW4MAdmin",
"MANAGER_VERSION_SUCCESS": "O IW4MAdmin está atualizado",
"MANAGER_VERSION_UPDATE": "Há uma atualização disponível. A versão mais recente é",
"PLUGIN_IMPORTER_NOTFOUND": "Não foram encontrados plugins para carregar",
"PLUGIN_IMPORTER_REGISTERCMD": "Comando registrado",
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "Inicie a sua sessão usando a senha",
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "Sua senha está errada",
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "Você agora está conectado",
"PLUGINS_STATS_COMMANDS_RESET_DESC": "reinicia suas estatísticas para uma nova",
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "Você deve estar conectado a um servidor para reiniciar as suas estatísticas",
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Suas estatísticas nesse servidor foram reiniciadas",
"PLUGINS_STATS_COMMANDS_TOP_DESC": "visualiza os 5 melhores jogadores do servidor",
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Top Jogadores",
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "mostra suas estatísticas",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "Não foi encontrado o jogador que você especificou",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "o jogador especificado deve estar dentro do jogo",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "Você deve estar no jogo para ver suas estatísticas",
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Estatísticas para",
"PLUGINS_STATS_TEXT_DEATHS": "MORTES",
"PLUGINS_STATS_TEXT_KILLS": "BAIXAS",
"PLUGINS_STATS_TEXT_NOQUALIFY": "Não há ainda jogadores qualificados para os primeiros lugares",
"PLUGINS_STATS_TEXT_SKILL": "HABILIDADE",
"SERVER_BAN_APPEAL": "apele em",
"SERVER_BAN_PREV": "Banido preventivamente por",
"SERVER_BAN_TEXT": "Você está banido",
"SERVER_ERROR_ADDPLAYER": "Não foi possível adicionar o jogador",
"SERVER_ERROR_COMMAND_INGAME": "Ocorreu um erro interno ao processar seu comando",
"SERVER_ERROR_COMMAND_LOG": "o comando gerou um erro",
"SERVER_ERROR_COMMUNICATION": "Não foi possível fazer a comunicação com",
"SERVER_ERROR_DNE": "não existe",
"SERVER_ERROR_DVAR": "Não foi possível obter o valor de dvar para",
"SERVER_ERROR_DVAR_HELP": "garanta que o servidor tenha um mapa carregado",
"SERVER_ERROR_EXCEPTION": "Exceção inesperada em",
"SERVER_ERROR_LOG": "Log do jogo inválido",
"SERVER_ERROR_PLUGIN": "Ocorreu um erro ao carregar o plug-in",
"SERVER_ERROR_POLLING": "reduzir a taxa de sondagem do server",
"SERVER_ERROR_UNFIXABLE": "Não monitorando o servidor devido a erros incorrigíveis",
"SERVER_KICK_CONTROLCHARS": "Seu nome não pode conter caracteres de controle",
"SERVER_KICK_GENERICNAME": "Por favor, mude o seu nome usando o comando /name no console",
"SERVER_KICK_MINNAME": "Seu nome deve conter no mínimo três caracteres",
"SERVER_KICK_NAME_INUSE": "Seu nome já está sendo usado por outra pessoa",
"SERVER_KICK_TEXT": "Você foi expulso",
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs não são permitidas neste servidor",
"SERVER_PLUGIN_ERROR": "Um plugin gerou erro",
"SERVER_REPORT_COUNT": "Você tem ^5{0} ^7denúncias recentes",
"SERVER_TB_REMAIN": "Você está banido temporariamente",
"SERVER_TB_TEXT": "Você está banido temporariamente",
"SERVER_WARNING": "AVISO",
"SERVER_WARNLIMT_REACHED": "Avisos demais! Leia o chat da próxima vez",
"SERVER_WEBSITE_GENERIC": "este é o site do servidor",
"SETUP_DISPLAY_SOCIAL": "Digitar link do convite do seu site no módulo da web (Discord, YouTube, etc.)",
"SETUP_ENABLE_CUSTOMSAY": "Habilitar a customização do nome do comando say",
"SETUP_ENABLE_MULTIOWN": "Habilitar vários proprietários",
"SETUP_ENABLE_STEPPEDPRIV": "Ativar hierarquia de privilégios escalonada",
"SETUP_ENABLE_VPNS": "Habilitar que os usuários usem VPN",
"SETUP_ENABLE_WEBFRONT": "Habilitar o módulo da web do IW4MAdmin",
"SETUP_ENCODING_STRING": "Digite sequência de codificação",
"SETUP_IPHUB_KEY": "Digite iphub.info api key",
"SETUP_SAY_NAME": "Habilitar a customização do nome do comando say",
"SETUP_SERVER_IP": "Digite o endereço IP do servidor",
"SETUP_SERVER_MANUALLOG": "Insira o caminho do arquivo de log manualmente",
"SETUP_SERVER_PORT": "Digite a porta do servidor",
"SETUP_SERVER_RCON": "Digite a senha do RCon do servidor",
"SETUP_SERVER_SAVE": "Configuração salva, adicionar outra",
"SETUP_SERVER_USEIW5M": "Usar analisador Pluto IW5 ",
"SETUP_SERVER_USET6M": "Usar analisador Pluto T6 ",
"SETUP_SOCIAL_LINK": "Digite o link da Rede Social",
"SETUP_SOCIAL_TITLE": "Digite o nome da rede social",
"SETUP_USE_CUSTOMENCODING": "Usar o analisador de codificação customizado",
"WEBFRONT_ACTION_BAN_NAME": "Banir",
"WEBFRONT_ACTION_LABEL_ID": "ID do cliente",
"WEBFRONT_ACTION_LABEL_PASSWORD": "Senha",
"WEBFRONT_ACTION_LABEL_REASON": "Razão",
"WEBFRONT_ACTION_LOGIN_NAME": "Iniciar a sessão",
"WEBFRONT_ACTION_UNBAN_NAME": "Retirar o banimento",
"WEBFRONT_CLIENT_META_FALSE": "Não está",
"WEBFRONT_CLIENT_META_JOINED": "Entrou com o nome",
"WEBFRONT_CLIENT_META_MASKED": "Mascarado",
"WEBFRONT_CLIENT_META_TRUE": "Está",
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Jogadores Privilegiados",
"WEBFRONT_CLIENT_PROFILE_TITLE": "Pefil",
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Jogadores correspondidos",
"WEBFRONT_CONSOLE_EXECUTE": "Executar",
"WEBFRONT_CONSOLE_TITLE": "Console da Web",
"WEBFRONT_ERROR_DESC": "O IW4MAdmin encontrou um erro",
"WEBFRONT_ERROR_GENERIC_DESC": "Ocorreu um erro ao processar seu pedido",
"WEBFRONT_ERROR_GENERIC_TITLE": "Desculpe!",
"WEBFRONT_ERROR_TITLE": "Erro!",
"WEBFRONT_HOME_TITLE": "Visão geral do servidor",
"WEBFRONT_NAV_CONSOLE": "Console",
"WEBFRONT_NAV_DISCORD": "Discord",
"WEBFRONT_NAV_HOME": "Início",
"WEBFRONT_NAV_LOGOUT": "Encerrar a sessão",
"WEBFRONT_NAV_PENALTIES": "Penalidades",
"WEBFRONT_NAV_PRIVILEGED": "Administradores",
"WEBFRONT_NAV_PROFILE": "Perfil do Jogador",
"WEBFRONT_NAV_SEARCH": "Achar jogador",
"WEBFRONT_NAV_SOCIAL": "Rede Social",
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Administrador",
"WEBFRONT_PENALTY_TEMPLATE_AGO": "atrás",
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Nome",
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Ofensa",
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "restantes",
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Mostrar",
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Mostrar somente",
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Tempo/Restante",
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Tipo",
"WEBFRONT_PENALTY_TITLE": "Penalidades dos jogadores",
"WEBFRONT_PROFILE_FSEEN": "Visto primeiro em",
"WEBFRONT_PROFILE_LEVEL": "Nível",
"WEBFRONT_PROFILE_LSEEN": "Visto por último em",
"WEBFRONT_PROFILE_PLAYER": "Jogou",
"PLUGIN_STATS_SETUP_ENABLEAC": "Habilitar a anti-trapaça no servidor (Somente IW4/MW2)",
"PLUGIN_STATS_ERROR_ADD": "Não foi possível adicionar o servidor para as estatísticas do servidor",
"PLUGIN_STATS_CHEAT_DETECTED": "Aparentemente você está trapaceando",
"PLUGINS_STATS_TEXT_KDR": "KDR",
"PLUGINS_STATS_META_SPM": "Pontuação por minuto",
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7 vem de ^5{{ClientLocation}}",
"PLUGINS_WELCOME_USERWELCOME": "Bem-vindo ^5{{ClientName}}^7, esta é a sua visita de número ^5{{TimesConnected}} ^7 no servidor!",
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} entrou no servidor",
"PLUGINS_LOGIN_AUTH": "não está registrado",
"PLUGINS_PROFANITY_SETUP_ENABLE": "Habilitar o plugin de anti-palavrão",
"PLUGINS_PROFANITY_WARNMSG": "Por favor, não use palavras ofensivas neste servidor",
"PLUGINS_PROFANITY_KICKMSG": "Uso excessivo de palavrão, lave a boca da próxima vez",
"GLOBAL_DEBUG": "Depuração",
"COMMANDS_UNFLAG_DESC": "Remover a sinalização do jogador",
"COMMANDS_UNFLAG_FAIL": "Você não pode retirar a sinalização do jogador",
"COMMANDS_UNFLAG_NOTFLAGGED": "O jogador não está sinalizado",
"COMMANDS_FLAG_ALREADYFLAGGED": "O jogador já está sinalizado",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Mais jogado",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "ver o top 5 de jogadores mais dedicados no servidor",
"WEBFRONT_PROFILE_MESSAGES": "Mensagens",
"WEBFRONT_CLIENT_META_CONNECTIONS": "Conexões",
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Classificação",
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Desempenho"
}
}
}

View File

@ -1,269 +0,0 @@
{
"LocalizationName": "ru-RU",
"LocalizationIndex": {
"Set": {
"BROADCAST_OFFLINE": "^5IW4MAdmin ^1ВЫКЛЮЧАЕТСЯ",
"BROADCAST_ONLINE": "^5IW4MADMIN ^7сейчас В СЕТИ",
"COMMAND_HELP_OPTIONAL": "опционально",
"COMMAND_HELP_SYNTAX": "Проблема с выражением мысли ( пересмотри слова)",
"COMMAND_MISSINGARGS": "Приведено недостаточно аргументов",
"COMMAND_NOACCESS": "У вас нет доступа к этой команде",
"COMMAND_NOTAUTHORIZED": "Вы не авторизованы для исполнения этой команды",
"COMMAND_TARGET_MULTI": "Несколько игроков соответствуют этому имени",
"COMMAND_TARGET_NOTFOUND": "Невозможно найти указанного игрока",
"COMMAND_UNKNOWN": "Вы ввели неизвестную команду",
"COMMANDS_ADMINS_DESC": "перечислить присоединенных на данный момент игроков с правами",
"COMMANDS_ADMINS_NONE": "Нет видимых администраторов в сети",
"COMMANDS_ALIAS_ALIASES": "Имена",
"COMMANDS_ALIAS_DESC": "получить прошлые имена и IP игрока",
"COMMANDS_ALIAS_IPS": "IP",
"COMMANDS_ARGS_CLEAR": "очистить",
"COMMANDS_ARGS_CLIENTID": "ID игрока",
"COMMANDS_ARGS_COMMANDS": "команды",
"COMMANDS_ARGS_DURATION": "длительность (m|h|d|w|y)",
"COMMANDS_ARGS_INACTIVE": "дни бездействия",
"COMMANDS_ARGS_LEVEL": "уровень",
"COMMANDS_ARGS_MAP": "карта",
"COMMANDS_ARGS_MESSAGE": "сообщение",
"COMMANDS_ARGS_PASSWORD": "пароль",
"COMMANDS_ARGS_PLAYER": "игрок",
"COMMANDS_ARGS_REASON": "причина",
"COMMANDS_BAN_DESC": "навсегда забанить игрока на сервере",
"COMMANDS_BAN_FAIL": "Вы не можете выдавать бан",
"COMMANDS_BAN_SUCCESS": "был забанен навсегда",
"COMMANDS_BANINFO_DESC": "получить информацию о бане игрока",
"COMMANDS_BANINFO_NONE": "Не найдено действующего бана для этого игрока",
"COMMANDS_BANINO_SUCCESS": "был забанен игроком ^5{0} ^7на:",
"COMMANDS_FASTRESTART_DESC": "перезапустить нынешнюю карту",
"COMMANDS_FASTRESTART_MASKED": "Карта была перезапущена",
"COMMANDS_FASTRESTART_UNMASKED": "перезапустил карту",
"COMMANDS_FIND_DESC": "найти игрока в базе данных",
"COMMANDS_FIND_EMPTY": "Не найдено игроков",
"COMMANDS_FIND_MIN": "Пожалуйста, введите хотя бы 3 символа",
"COMMANDS_FLAG_DESC": "отметить подозрительного игрока и сообщить администраторам, чтобы присоединились",
"COMMANDS_FLAG_FAIL": "Вы не можете ставить отметки",
"COMMANDS_FLAG_SUCCESS": "Вы отметили",
"COMMANDS_FLAG_UNFLAG": "Вы сняли отметку",
"COMMANDS_HELP_DESC": "перечислить все доступные команды",
"COMMANDS_HELP_MOREINFO": "Введите !help <имя команды>, чтобы узнать синтаксис для использования команды",
"COMMANDS_HELP_NOTFOUND": "Не удалось найти эту команду",
"COMMANDS_IP_DESC": "просмотреть ваш внешний IP-адрес",
"COMMANDS_IP_SUCCESS": "Ваш внешний IP:",
"COMMANDS_KICK_DESC": "исключить игрока по имени",
"COMMANDS_KICK_FAIL": "У вас нет достаточных прав, чтобы исключать",
"COMMANDS_KICK_SUCCESS": "был исключен",
"COMMANDS_LIST_DESC": "перечислить действующих игроков",
"COMMANDS_MAP_DESC": "сменить на определенную карту",
"COMMANDS_MAP_SUCCESS": "Смена карты на",
"COMMANDS_MAP_UKN": "Попытка сменить на неизвестную карту",
"COMMANDS_MAPROTATE": "Смена карты через ^55 ^7секунд",
"COMMANDS_MAPROTATE_DESC": "переключиться на следующую карту в ротации",
"COMMANDS_MASK_DESC": "скрыть свое присутствие как игрока с правами",
"COMMANDS_MASK_OFF": "Вы теперь демаскированы",
"COMMANDS_MASK_ON": "Вы теперь замаскированы",
"COMMANDS_OWNER_DESC": "утверить владение сервером",
"COMMANDS_OWNER_FAIL": "Этот сервер уже имеет владельца",
"COMMANDS_OWNER_SUCCESS": "Поздравляю, вы утвердили владение этим сервером!",
"COMMANDS_PASSWORD_FAIL": "Ваш пароль должен быть хотя бы 5 символов в длину",
"COMMANDS_PASSWORD_SUCCESS": "Ваш пароль был успешно установлен",
"COMMANDS_PING_DESC": "получить пинг игрока",
"COMMANDS_PING_SELF": "Ваш пинг:",
"COMMANDS_PING_TARGET": "пинг:",
"COMMANDS_PLUGINS_DESC": "просмотреть все загруженные плагины",
"COMMANDS_PLUGINS_LOADED": "Загруженные плагины",
"COMMANDS_PM_DESC": "отправить сообщение другому игроку",
"COMMANDS_PRUNE_DESC": "понизить любых игроков с правами, которые не подключались за последнее время (по умолчанию: 30 дней)",
"COMMANDS_PRUNE_FAIL": "Неверное количество дней бездействия",
"COMMANDS_PRUNE_SUCCESS": "бездействующих пользователей с правами было сокращено",
"COMMANDS_QUIT_DESC": "покинуть IW4MAdmin",
"COMMANDS_RCON_DESC": "отправить RCon команду на сервер",
"COMMANDS_RCON_SUCCESS": "Успешно отправлена команда RCon",
"COMMANDS_REPORT_DESC": "пожаловаться на игрока за подозрительное поведение",
"COMMANDS_REPORT_FAIL": "Вы не можете пожаловаться",
"COMMANDS_REPORT_FAIL_CAMP": "Вы не можете пожаловаться на игрока за кемперство",
"COMMANDS_REPORT_FAIL_DUPLICATE": "Вы уже пожаловались на этого игрока",
"COMMANDS_REPORT_FAIL_SELF": "Вы не можете пожаловаться на самого себя",
"COMMANDS_REPORT_SUCCESS": "Спасибо за вашу жалобу, администратор оповещен",
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Жалобы успешно очищены",
"COMMANDS_REPORTS_DESC": "получить или очистить последние жалобы",
"COMMANDS_REPORTS_NONE": "Пока нет жалоб на игроков",
"COMMANDS_RULES_DESC": "перечислить правила сервера",
"COMMANDS_RULES_NONE": "Владелец сервера не установил никаких правил",
"COMMANDS_SAY_DESC": "транслировать сообщения всем игрокам",
"COMMANDS_SETLEVEL_DESC": "установить особый уровень прав игроку",
"COMMANDS_SETLEVEL_FAIL": "Указана неверная группа",
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "Вы только можете повысить ^5{0} ^7до ^5{1} ^7или понизить в правах",
"COMMANDS_SETLEVEL_OWNER": "Может быть только 1 владелец. Измените настройки, если требуется несколько владельцов",
"COMMANDS_SETLEVEL_SELF": "Вы не можете изменить свой уровень",
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "Этот сервер не разрешает вам повыситься",
"COMMANDS_SETLEVEL_SUCCESS": "был успешно повышен",
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Поздравляю! Вы были повышены до",
"COMMANDS_SETPASSWORD_DESC": "установить свой пароль аутентификации",
"COMMANDS_TEMPBAN_DESC": "временно забанить игрока на определенное время (по умолчанию: 1 час)",
"COMMANDS_TEMPBAN_FAIL": "Вы не можете выдавать временный бан",
"COMMANDS_TEMPBAN_SUCCESS": "был временно забанен за",
"COMMANDS_UNBAN_DESC": "разбанить игрока по ID игрока",
"COMMANDS_UNBAN_FAIL": "не забанен",
"COMMANDS_UNBAN_SUCCESS": "Успешно разбанен",
"COMMANDS_UPTIME_DESC": "получить время с начала запуска текущего приложения",
"COMMANDS_UPTIME_TEXT": "был в сети",
"COMMANDS_USAGE_DESC": "узнать о потреблении памяти приложением",
"COMMANDS_USAGE_TEXT": "используется",
"COMMANDS_WARN_DESC": "предупредить игрока за нарушение правил",
"COMMANDS_WARN_FAIL": "У вас недостаточно прав, чтобы выносить предупреждения",
"COMMANDS_WARNCLEAR_DESC": "удалить все предупреждения у игрока",
"COMMANDS_WARNCLEAR_SUCCESS": "Все предупреждения очищены у",
"COMMANDS_WHO_DESC": "предоставить информацию о себе",
"GLOBAL_DAYS": "дней",
"GLOBAL_ERROR": "Ошибка",
"GLOBAL_HOURS": "часов",
"GLOBAL_INFO": "Информация",
"GLOBAL_MINUTES": "минут",
"GLOBAL_REPORT": "Если вы подозреваете кого-то в ^5ЧИТЕРСТВЕ^7, используйте команду ^5!report",
"GLOBAL_VERBOSE": "Подробно",
"GLOBAL_WARNING": "Предупреждение",
"MANAGER_CONNECTION_REST": "Соединение было восстановлено с помощью",
"MANAGER_CONSOLE_NOSERV": "На данный момент нет серверов под мониторингом",
"MANAGER_EXIT": "Нажмите любую клавишу, чтобы выйти...",
"MANAGER_INIT_FAIL": "Критическая ошибка во время инициализации",
"MANAGER_MONITORING_TEXT": "Идет мониторинг",
"MANAGER_SHUTDOWN_SUCCESS": "Выключение завершено",
"MANAGER_VERSION_CURRENT": "Ваша версия:",
"MANAGER_VERSION_FAIL": "Не удалось получить последнюю версию IW4MAdmin",
"MANAGER_VERSION_SUCCESS": "IW4MAdmin обновлен",
"MANAGER_VERSION_UPDATE": "- есть обновление. Последняя версия:",
"PLUGIN_IMPORTER_NOTFOUND": "Не найдено плагинов для загрузки",
"PLUGIN_IMPORTER_REGISTERCMD": "Зарегистрированная команда",
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "войти, используя пароль",
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "Ваш пароль неверный",
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "Вы теперь вошли",
"PLUGINS_STATS_COMMANDS_RESET_DESC": "сбросить вашу статистику под ноль",
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "Вы должны быть подключены к серверу, чтобы сбросить свою статистику",
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Ваша статистика на этом сервере была сброшена",
"PLUGINS_STATS_COMMANDS_TOP_DESC": "показать топ-5 лучших игроков на этом сервере",
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Лучшие игроки",
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "просмотреть свою статистику",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "Не удается найти игрока, которого вы указали.",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "Указанный игрок должен быть в игре",
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "Вы должны быть в игре, чтобы просмотреть свою статистику",
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Статистика",
"PLUGINS_STATS_TEXT_DEATHS": "СМЕРТЕЙ",
"PLUGINS_STATS_TEXT_KILLS": "УБИЙСТВ",
"PLUGINS_STATS_TEXT_NOQUALIFY": "Ещё нет совернующихся игроков за лучшую статистику",
"PLUGINS_STATS_TEXT_SKILL": "МАСТЕРСТВО",
"SERVER_BAN_APPEAL": "оспорить:",
"SERVER_BAN_PREV": "Ранее забанены за",
"SERVER_BAN_TEXT": "Вы забанены",
"SERVER_ERROR_ADDPLAYER": "Не удалось добавить игрока",
"SERVER_ERROR_COMMAND_INGAME": "Произошла внутренняя ошибка при обработке вашей команды",
"SERVER_ERROR_COMMAND_LOG": "команда сгенерировала ошибку",
"SERVER_ERROR_COMMUNICATION": "Не удалось связаться с",
"SERVER_ERROR_DNE": "не существует",
"SERVER_ERROR_DVAR": "Не удалось получить значение dvar:",
"SERVER_ERROR_DVAR_HELP": "убедитесь, что на сервере загружена карта",
"SERVER_ERROR_EXCEPTION": "Неожиданное исключение на",
"SERVER_ERROR_LOG": "Неверный игровой лог-файл",
"SERVER_ERROR_PLUGIN": "Произошла ошибка загрузки плагина",
"SERVER_ERROR_POLLING": "снижение частоты обновления данных",
"SERVER_ERROR_UNFIXABLE": "Мониторинг сервера выключен из-за неисправимых ошибок",
"SERVER_KICK_CONTROLCHARS": "Ваше имя не должно содержать спецсимволы",
"SERVER_KICK_GENERICNAME": "Пожалуйста, смените ваше имя, используя /name",
"SERVER_KICK_MINNAME": "Ваше имя должно содержать хотя бы 3 символа",
"SERVER_KICK_NAME_INUSE": "Ваше имя используется кем-то другим",
"SERVER_KICK_TEXT": "Вы были исключены",
"SERVER_KICK_VPNS_NOTALLOWED": "Использование VPN не разрешено на этом сервере",
"SERVER_PLUGIN_ERROR": "Плагин образовал ошибку",
"SERVER_REPORT_COUNT": "Имеется ^5{0} ^7жалоб за последнее время",
"SERVER_TB_REMAIN": "Вы временно забанены",
"SERVER_TB_TEXT": "Вы временно забанены",
"SERVER_WARNING": "ПРЕДУПРЕЖДЕНИЕ",
"SERVER_WARNLIMT_REACHED": "Слишком много предупреждений",
"SERVER_WEBSITE_GENERIC": "веб-сайт этого сервера",
"SETUP_DISPLAY_SOCIAL": "Отображать ссылку на социальную сеть в веб-интерфейсе (Discord, веб-сайт, ВК, и т.д.)",
"SETUP_ENABLE_CUSTOMSAY": "Включить кастомное имя для чата",
"SETUP_ENABLE_MULTIOWN": "Включить поддержку нескольких владельцев",
"SETUP_ENABLE_STEPPEDPRIV": "Включить последовательную иерархию прав",
"SETUP_ENABLE_VPNS": "Включить поддержку VPN у игроков",
"SETUP_ENABLE_WEBFRONT": "Включить веб-интерфейс",
"SETUP_ENCODING_STRING": "Введите кодировку",
"SETUP_IPHUB_KEY": "Введите iphub.info api-ключ",
"SETUP_SAY_NAME": "Введите кастомное имя для чата",
"SETUP_SERVER_IP": "Введите IP-адрес сервера",
"SETUP_SERVER_MANUALLOG": "Введите путь для лог-файла",
"SETUP_SERVER_PORT": "Введите порт сервера",
"SETUP_SERVER_RCON": "Введите RCon пароль сервера",
"SETUP_SERVER_SAVE": "Настройки сохранены, добавить",
"SETUP_SERVER_USEIW5M": "Использовать парсер Pluto IW5",
"SETUP_SERVER_USET6M": "Использовать парсер Pluto T6",
"SETUP_SOCIAL_LINK": "Ввести ссылку на социальную сеть",
"SETUP_SOCIAL_TITLE": "Ввести имя социальной сети",
"SETUP_USE_CUSTOMENCODING": "Использовать кастомную кодировку парсера",
"WEBFRONT_ACTION_BAN_NAME": "Забанить",
"WEBFRONT_ACTION_LABEL_ID": "ID игрока",
"WEBFRONT_ACTION_LABEL_PASSWORD": "Пароль",
"WEBFRONT_ACTION_LABEL_REASON": "Причина",
"WEBFRONT_ACTION_LOGIN_NAME": "Войти",
"WEBFRONT_ACTION_UNBAN_NAME": "Разбанить",
"WEBFRONT_CLIENT_META_FALSE": "не",
"WEBFRONT_CLIENT_META_JOINED": "Присоединился с именем",
"WEBFRONT_CLIENT_META_MASKED": "Замаскирован",
"WEBFRONT_CLIENT_META_TRUE": "Это",
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Игроки с правами",
"WEBFRONT_CLIENT_PROFILE_TITLE": "Профиль",
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Подходящие игроки",
"WEBFRONT_CONSOLE_EXECUTE": "Выполнить",
"WEBFRONT_CONSOLE_TITLE": "Веб-консоль",
"WEBFRONT_ERROR_DESC": "IW4MAdmin столкнулся с ошибкой",
"WEBFRONT_ERROR_GENERIC_DESC": "Произошла ошибка во время обработки вашего запроса",
"WEBFRONT_ERROR_GENERIC_TITLE": "Извините!",
"WEBFRONT_ERROR_TITLE": "Ошибка!",
"WEBFRONT_HOME_TITLE": "Обзор сервера",
"WEBFRONT_NAV_CONSOLE": "Консоль",
"WEBFRONT_NAV_DISCORD": "Дискорд ",
"WEBFRONT_NAV_HOME": "Обзор Серверов ",
"WEBFRONT_NAV_LOGOUT": "Выйти",
"WEBFRONT_NAV_PENALTIES": "Наказания",
"WEBFRONT_NAV_PRIVILEGED": "Админы",
"WEBFRONT_NAV_PROFILE": "Профиль игрока",
"WEBFRONT_NAV_SEARCH": "Найти игрока",
"WEBFRONT_NAV_SOCIAL": "Соц. сети",
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Админ",
"WEBFRONT_PENALTY_TEMPLATE_AGO": "назад",
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Имя",
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Нарушение",
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "осталось",
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Показывать",
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Показывать только",
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Время/Осталось",
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Тип",
"WEBFRONT_PENALTY_TITLE": "Наказания игроков",
"WEBFRONT_PROFILE_FSEEN": "Впервые заходил",
"WEBFRONT_PROFILE_LEVEL": "Уровень",
"WEBFRONT_PROFILE_LSEEN": "Последний раз заходил",
"WEBFRONT_PROFILE_PLAYER": "Наиграл",
"PLUGIN_STATS_SETUP_ENABLEAC": "Включить серверный античит (только IW4)",
"PLUGIN_STATS_ERROR_ADD": "Не удалось добавить сервер в статистику серверов",
"PLUGIN_STATS_CHEAT_DETECTED": "Кажется, вы читерите",
"PLUGINS_STATS_TEXT_KDR": "Вот так ..",
"PLUGINS_STATS_META_SPM": "Счёт за минуту",
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7из ^5{{ClientLocation}}",
"PLUGINS_WELCOME_USERWELCOME": "Добро пожаловать, ^5{{ClientName}}^7. Это ваше ^5{{TimesConnected}} ^7подключение по счёту!",
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} присоединился к серверу",
"PLUGINS_LOGIN_AUTH": "Сперва Подключись",
"PLUGINS_PROFANITY_SETUP_ENABLE": "Включить сдерживание ненормативной лексики",
"PLUGINS_PROFANITY_WARNMSG": "Пожалуйта, не ругайтесь на этом сервере",
"PLUGINS_PROFANITY_KICKMSG": "Чрезмерное употребление ненормативной лексики",
"GLOBAL_DEBUG": "Отлаживание ",
"COMMANDS_UNFLAG_DESC": "Снять все подозрение с игрока !",
"COMMANDS_UNFLAG_FAIL": "Вы не можете снять подозрения..",
"COMMANDS_UNFLAG_NOTFLAGGED": "Игрок без подозрения !",
"COMMANDS_FLAG_ALREADYFLAGGED": "Игрок помечен ! ",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Самые популярные",
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "просмотр 5 лучших игроков на сервере",
"WEBFRONT_PROFILE_MESSAGES": "Сообщения",
"WEBFRONT_CLIENT_META_CONNECTIONS": "Подключения",
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Рейтинг",
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Эффективность"
}
}
}

View File

@ -1,90 +0,0 @@
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.IO;
namespace IW4MAdmin.Application
{
class Logger : SharedLibraryCore.Interfaces.ILogger
{
enum LogType
{
Verbose,
Info,
Debug,
Warning,
Error,
Assert
}
string FileName;
object ThreadLock;
public Logger(string fn)
{
FileName = fn;
ThreadLock = new object();
if (File.Exists(fn))
File.Delete(fn);
}
void Write(string msg, LogType type)
{
string stringType = type.ToString();
try
{
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
}
catch (Exception) { }
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {stringType}: {msg}";
lock (ThreadLock)
{
#if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(LogLine);
File.AppendAllText(FileName, LogLine + Environment.NewLine);
#else
if (type == LogType.Error || type == LogType.Verbose)
Console.WriteLine(LogLine);
//if (type != LogType.Debug)
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
}
public void WriteVerbose(string msg)
{
Write(msg, LogType.Verbose);
}
public void WriteDebug(string msg)
{
Write(msg, LogType.Debug);
}
public void WriteError(string msg)
{
Write(msg, LogType.Error);
}
public void WriteInfo(string msg)
{
Write(msg, LogType.Info);
}
public void WriteWarning(string msg)
{
Write(msg, LogType.Warning);
}
public void WriteAssert(bool condition, string msg)
{
if (!condition)
Write(msg, LogType.Assert);
}
}
}

View File

@ -1,186 +1,489 @@
using System;
using System.Threading.Tasks;
using System.IO;
using System.Reflection;
using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Database;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services;
using Stats.Dtos;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Collections.Generic;
using SharedLibraryCore.Localization;
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.IO;
using IW4MAdmin.Application.Localization;
using IW4MAdmin.Application.Plugin;
using IW4MAdmin.Application.Plugin.Script;
using IW4MAdmin.Application.QueryHelpers;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
using WebfrontCore.QueryHelpers.Models;
namespace IW4MAdmin.Application
{
public class Program
{
static public double Version { get; private set; }
static public ApplicationManager ServerManager = ApplicationManager.GetInstance();
public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager;
private static Task _applicationTask;
private static IServiceProvider _serviceProvider;
public static void Main(string[] args)
/// <summary>
/// entrypoint of the application
/// </summary>
/// <returns></returns>
public static async Task Main(string[] args)
{
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
//System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
Version = Math.Round(Version, 2);
Console.CancelKeyPress += OnCancelKey;
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4M ADMIN");
Console.WriteLine(" IW4MAdmin");
Console.WriteLine(" by RaidMax ");
Console.WriteLine($" Version {Version.ToString("0.0")}");
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
Index loc = null;
await LaunchAsync(args);
}
/// <summary>
/// event callback executed when the control + c combination is detected
/// gracefully stops the server manager and waits for all tasks to finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{
if (_serverManager is not null)
{
await _serverManager.Stop();
}
if (_applicationTask is not null)
{
await _applicationTask;
}
}
/// <summary>
/// task that initializes application and starts the application monitoring and runtime tasks
/// </summary>
/// <returns></returns>
private static async Task LaunchAsync(string[] args)
{
restart:
ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
try
{
CheckDirectories();
// do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
ServerManager = ApplicationManager.GetInstance();
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
loc = Utilities.CurrentLocalization.LocalizationIndex;
logger.LogDebug("Configuring services...");
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration()?.ConnectionString))
new ContextSeed(db).Seed().Wait();
var configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await configHandler.BuildAsync();
_serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
(configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
var api = API.Master.Endpoint.Get();
_serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
var version = new API.Master.VersionInfo()
{
CurrentVersionStable = 99.99f
};
await _serverManager.Init();
try
{
version = api.GetVersion().Result;
_applicationTask = Task.WhenAll(RunApplicationTasksAsync(logger, _serviceProvider),
_serverManager.Start());
await _applicationTask;
logger.LogInformation("Shutdown completed successfully");
}
catch (Exception e)
{
ServerManager.Logger.WriteWarning(loc["MANAGER_VERSION_FAIL"]);
var failMessage = translationLookup == null
? "Failed to initialize IW4MAdmin"
: translationLookup["MANAGER_INIT_FAIL"];
var exitMessage = translationLookup == null
? "Press enter to exit..."
: translationLookup["MANAGER_EXIT"];
logger.LogCritical(e, "Failed to initialize IW4MAdmin");
Console.WriteLine(failMessage);
while (e.InnerException != null)
{
e = e.InnerException;
}
ServerManager.Logger.WriteDebug(e.Message);
if (e is ConfigurationException configException)
{
Console.WriteLine("{{fileName}} contains an error."
.FormatExt(Path.GetFileName(configException.ConfigurationFileName)));
foreach (var error in configException.Errors)
{
Console.WriteLine(error);
}
}
if (version.CurrentVersionStable == 99.99f)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}]");
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}-pr]");
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine(e.Message);
}
ServerManager.Init().Wait();
var consoleTask = Task.Run(() =>
if (_serverManager is not null)
{
String userInput;
Player Origin = ServerManager.GetClientService().Get(1).Result.AsPlayer();
do
{
userInput = Console.ReadLine();
if (userInput?.ToLower() == "quit")
ServerManager.Stop();
if (ServerManager.Servers.Count == 0)
{
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
continue;
await _serverManager?.Stop();
}
if (userInput?.Length > 0)
{
Origin.CurrentServer = ServerManager.Servers[0];
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = userInput,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.GetEventHandler().AddEvent(E);
E.OnProcessed.Wait(5000);
}
Console.Write('>');
} while (ServerManager.Running);
});
}
catch (Exception e)
{
Console.WriteLine(loc["MANAGER_INIT_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
Console.WriteLine($"Exception: {e.Message}");
Console.WriteLine(loc["MANAGER_EXIT"]);
Console.ReadKey();
Console.WriteLine(exitMessage);
await Console.In.ReadAsync(new char[1], 0, 1);
return;
}
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
if (_serverManager.IsRestartRequested)
{
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
goto restart;
}
}
OnShutdownComplete.Reset();
ServerManager.Start().Wait();
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
OnShutdownComplete.Set();
}
private static void OnCancelKey(object sender, ConsoleCancelEventArgs e)
/// <summary>
/// runs the core application tasks
/// </summary>
/// <returns></returns>
private static Task RunApplicationTasksAsync(ILogger logger, IServiceProvider serviceProvider)
{
ServerManager.Stop();
OnShutdownComplete.Wait(5000);
}
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken)
: Task.CompletedTask;
static void CheckDirectories()
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
async void ReadInput() => await ReadConsoleInput(logger);
var inputThread = new Thread(ReadInput);
inputThread.Start();
var tasks = new[]
{
string curDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
versionChecker.CheckVersion(),
webfrontTask,
serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
};
if (!Directory.Exists($"{curDirectory}Plugins"))
Directory.CreateDirectory($"{curDirectory}Plugins");
logger.LogDebug("Starting webfront and input tasks");
return Task.WhenAll(tasks);
}
/// <summary>
/// reads input from the console and executes entered commands on the default server
/// </summary>
/// <returns></returns>
private static async Task ReadConsoleInput(ILogger logger)
{
if (Console.IsInputRedirected)
{
logger.LogInformation("Disabling console input as it has been redirected");
return;
}
EFClient origin = null;
try
{
while (!_serverManager.CancellationToken.IsCancellationRequested)
{
if (!_serverManager.IsInitialized)
{
await Task.Delay(1000);
continue;
}
var lastCommand = await Console.In.ReadLineAsync();
if (lastCommand == null)
{
continue;
}
if (!lastCommand.Any())
{
continue;
}
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = origin ??= Utilities.IW4MAdminClient(_serverManager.Servers.FirstOrDefault()),
Owner = _serverManager.Servers[0]
};
_serverManager.AddEvent(gameEvent);
await gameEvent.WaitAsync(Utilities.DefaultCommandTimeout, _serverManager.CancellationToken);
Console.Write('>');
}
}
catch (OperationCanceledException)
{
}
}
private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
IServiceCollection serviceCollection,
IMasterApi masterApi)
{
var defaultLogger = BuildDefaultLogger<Program>(appConfig);
var pluginServiceProvider = new ServiceCollection()
.AddBaseLogger(appConfig)
.AddSingleton(appConfig)
.AddSingleton(masterApi)
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IPluginImporter, PluginImporter>()
.BuildServiceProvider();
var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>();
// we need to register the rest client with regular collection
serviceCollection.AddSingleton(masterApi);
// register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Concat(typeof(Program).Assembly.GetTypes().Where(type => type.Namespace?.StartsWith("IW4MAdmin.Application.Commands") ?? false))
.Where(command => command.BaseType == typeof(Command)))
{
defaultLogger.LogDebug("Registered native command type {Name}", commandType.Name);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register the plugin implementations
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null;
defaultLogger.LogDebug("Registering plugin type {Name}", pluginType.FullName);
serviceCollection.AddSingleton(!isV2 ? typeof(IPlugin) : typeof(IPluginV2), pluginType);
try
{
var registrationMethod = pluginType.GetMethod(nameof(IPluginV2.RegisterDependencies));
registrationMethod?.Invoke(null, new object[] { serviceCollection });
}
catch (Exception ex)
{
defaultLogger.LogError(ex, "Could not register plugin of type {Type}", pluginType.Name);
}
}
// register the plugin commands
foreach (var commandType in commands)
{
defaultLogger.LogDebug("Registered plugin command type {Name}", commandType.FullName);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
foreach (var configurationType in configurations)
{
defaultLogger.LogDebug("Registered plugin config type {Name}", configurationType.Name);
var configInstance = (IBaseConfiguration) Activator.CreateInstance(configurationType);
var handlerType = typeof(BaseConfigurationHandler<>).MakeGenericType(configurationType);
var handlerInstance = Activator.CreateInstance(handlerType, configInstance.Name());
var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
}
var scriptPlugins = pluginImporter.DiscoverScriptPlugins();
foreach (var scriptPlugin in scriptPlugins)
{
serviceCollection.AddSingleton(scriptPlugin.Item1, sp =>
sp.GetRequiredService<IScriptPluginFactory>()
.CreateScriptPlugin(scriptPlugin.Item1, scriptPlugin.Item2));
}
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))
.Union(plugins.SelectMany(asm => asm.Assembly.GetTypes())
.Distinct()
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
return serviceCollection;
}
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static void ConfigureServices(IServiceCollection serviceCollection)
{
// todo: this is a quick fix
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
.AddConfiguration<DefaultSettings>()
.AddConfiguration<CommandConfiguration>()
.AddConfiguration<StatsConfiguration>("StatsPluginSettings");
// for legacy purposes. update at some point
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var httpClient = new HttpClient
{
BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15)
};
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null)
{
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig);
appConfigHandler.Save().GetAwaiter().GetResult();
}
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
if (!Utilities.PermissionLevelOverrides.ContainsKey(key))
{
Utilities.PermissionLevelOverrides.Add(key, value);
}
}
// build the dependency list
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
.AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.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>()
.AddSingleton<ChangeHistoryService>()
.AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>,
ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>,
AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>, ClientResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>()
.AddSingleton<IManager, ApplicationManager>()
#pragma warning disable CS0612
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
#pragma warning restore CS0612
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton<IClientStatisticCalculator, HitCalculator>()
.AddSingleton<IServerDistributionCalculator, ServerDistributionCalculator>()
.AddSingleton<IWeaponNameParser, WeaponNameParser>()
.AddSingleton<IHitInfoBuilder, HitInfoBuilder>()
.AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
#pragma warning disable CS0618
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
#pragma warning restore CS0618
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton<IRemoteCommandService, RemoteCommandService>()
.AddSingleton(new ConfigurationWatcher())
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>();
serviceCollection.AddSource();
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
}
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
{
var collection = new ServiceCollection()
.AddBaseLogger(appConfig)
.BuildServiceProvider();
return collection.GetRequiredService<ILogger<T>>();
}
}
}

View File

@ -1,514 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.IO;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using IW4MAdmin.Application.API;
using Microsoft.Extensions.Configuration;
using WebfrontCore;
using SharedLibraryCore.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;
using IW4MAdmin.Application.API.Master;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private List<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public Dictionary<int, Player> PrivilegedClients { get; set; }
public ILogger Logger { get; private set; }
public bool Running { get; private set; }
public EventHandler<GameEvent> ServerEventOccurred { get; private set; }
public DateTime StartTime { get; private set; }
static ApplicationManager Instance;
List<AsyncStatus> TaskStatuses;
List<Command> Commands;
List<MessageToken> MessageTokens;
ClientService ClientSvc;
AliasService AliasSvc;
PenaltyService PenaltySvc;
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
EventApi Api;
GameEventHandler Handler;
ManualResetEventSlim OnEvent;
private ApplicationManager()
{
Logger = new Logger($@"{Utilities.OperatingDirectory}IW4MAdmin.log");
_servers = new List<Server>();
Commands = new List<Command>();
TaskStatuses = new List<AsyncStatus>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
PrivilegedClients = new Dictionary<int, Player>();
Api = new EventApi();
ServerEventOccurred += Api.OnServerEvent;
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
StartTime = DateTime.UtcNow;
OnEvent = new ManualResetEventSlim();
}
public IList<Server> GetServers()
{
return Servers;
}
public IList<Command> GetCommands()
{
return Commands;
}
public static ApplicationManager GetInstance()
{
return Instance ?? (Instance = new ApplicationManager());
}
public async Task UpdateStatus(object state)
{
var taskList = new List<Task>();
while (Running)
{
taskList.Clear();
foreach (var server in Servers)
{
taskList.Add(Task.Run(async () =>
{
try
{
await server.ProcessUpdatesAsync(new CancellationToken());
}
catch (Exception e)
{
Logger.WriteWarning($"Failed to update status for {server}");
Logger.WriteDebug($"Exception: {e.Message}");
Logger.WriteDebug($"StackTrace: {e.StackTrace}");
}
}));
}
#if DEBUG
Logger.WriteDebug($"{taskList.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
await Task.WhenAll(taskList.ToArray());
GameEvent sensitiveEvent;
while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null)
{
try
{
await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent);
#if DEBUG
Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}");
#endif
}
catch (NetworkException e)
{
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]);
Logger.WriteDebug(e.Message);
}
catch (Exception E)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
}
sensitiveEvent.OnProcessed.Set();
}
await Task.Delay(2500);
}
}
public async Task Init()
{
Running = true;
#region DATABASE
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new
{
c.Password,
c.PasswordSalt,
c.ClientId,
c.Level,
c.Name
});
foreach (var a in ipList)
{
try
{
PrivilegedClients.Add(a.ClientId, new Player()
{
Name = a.Name,
ClientId = a.ClientId,
Level = a.Level,
PasswordSalt = a.PasswordSalt,
Password = a.Password
});
}
catch (ArgumentException)
{
continue;
}
}
#endregion
#region CONFIG
var config = ConfigHandler.Configuration();
// copy over default config if it doesn't exist
if (config == null)
{
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessagePeriod = defaultConfig.AutoMessagePeriod;
newConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps;
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = new List<ServerConfiguration>();
do
{
newConfig.Servers.Add((ServerConfiguration)new ServerConfiguration().Generate());
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
config = newConfig;
await ConfigHandler.Save();
}
}
else if (config != null)
{
if (string.IsNullOrEmpty(config.Id))
{
config.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{
config.WebfrontBindUrl = "http://127.0.0.1:1624";
await ConfigHandler.Save();
}
}
else if (config.Servers.Count == 0)
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
#endregion
#region PLUGINS
SharedLibraryCore.Plugins.PluginImporter.Load(this);
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
try
{
await Plugin.OnLoadAsync(this);
}
catch (Exception e)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}");
Logger.WriteDebug($"Exception: {e.Message}");
Logger.WriteDebug($"Stack Trace: {e.StackTrace}");
}
}
#endregion
#region COMMANDS
if (ClientSvc.GetOwners().Result.Count == 0)
Commands.Add(new COwner());
Commands.Add(new CQuit());
Commands.Add(new CKick());
Commands.Add(new CSay());
Commands.Add(new CTempBan());
Commands.Add(new CBan());
Commands.Add(new CWhoAmI());
Commands.Add(new CList());
Commands.Add(new CHelp());
Commands.Add(new CFastRestart());
Commands.Add(new CMapRotate());
Commands.Add(new CSetLevel());
Commands.Add(new CUsage());
Commands.Add(new CUptime());
Commands.Add(new CWarn());
Commands.Add(new CWarnClear());
Commands.Add(new CUnban());
Commands.Add(new CListAdmins());
Commands.Add(new CLoadMap());
Commands.Add(new CFindPlayer());
Commands.Add(new CListRules());
Commands.Add(new CPrivateMessage());
Commands.Add(new CFlag());
Commands.Add(new CUnflag());
Commands.Add(new CReport());
Commands.Add(new CListReports());
Commands.Add(new CListBanInfo());
Commands.Add(new CListAlias());
Commands.Add(new CExecuteRCON());
Commands.Add(new CPlugins());
Commands.Add(new CIP());
Commands.Add(new CMask());
Commands.Add(new CPruneAdmins());
Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
Commands.Add(C);
#endregion
#region INIT
async Task Init(ServerConfiguration Conf)
{
// setup the event handler after the class is initialized
Handler = new GameEventHandler(this);
try
{
var ServerInstance = new IW4MServer(this, Conf);
await ServerInstance.Initialize();
lock (_servers)
{
_servers.Add(ServerInstance);
}
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
// add the start event for this server
Handler.AddEvent(new GameEvent(GameEvent.EventType.Start, "Server started", null, null, ServerInstance));
}
catch (ServerException e)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"]} [{Conf.IPAddress}:{Conf.Port}]");
if (e.GetType() == typeof(DvarException))
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"]} {(e as DvarException).Data["dvar_name"]} ({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})");
else if (e.GetType() == typeof(NetworkException))
{
Logger.WriteDebug(e.Message);
}
// throw the exception to the main method to stop before instantly exiting
throw e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
#endregion
}
private async Task SendHeartbeat(object state)
{
var heartbeatState = (HeartbeatState)state;
while (Running)
{
if (!heartbeatState.Connected)
{
try
{
await Heartbeat.Send(this, true);
heartbeatState.Connected = true;
}
catch (Exception e)
{
heartbeatState.Connected = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
}
else
{
try
{
await Heartbeat.Send(this);
}
catch (System.Net.Http.HttpRequestException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
catch (AggregateException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException));
foreach (var ex in exceptions)
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
heartbeatState.Connected = false;
}
}
}
catch (RestEase.ApiException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
heartbeatState.Connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
}
await Task.Delay(30000);
}
}
public async Task Start()
{
// this needs to be run seperately from the main thread
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
#if !DEBUG
// start heartbeat
Task.Run(() => SendHeartbeat(new HeartbeatState()));
#endif
Task.Run(() => UpdateStatus(null));
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
var eventList = new List<Task>();
async Task processEvent(GameEvent newEvent)
{
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
#if DEBUG
Logger.WriteDebug("Processed Event");
#endif
}
// this happens if a plugin requires login
catch (AuthorizationException e)
{
await newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
}
catch (NetworkException e)
{
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]);
Logger.WriteDebug(e.Message);
}
catch (Exception E)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
}
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
};
GameEvent queuedEvent = null;
while (Running)
{
// wait for new event to be added
OnEvent.Wait();
// todo: sequencially or parallelize?
while ((queuedEvent = Handler.GetNextEvent()) != null)
{
await processEvent(queuedEvent);
}
// this should allow parallel processing of events
// await Task.WhenAll(eventList);
// signal that all events have been processed
OnEvent.Reset();
}
#if !DEBUG
foreach (var S in _servers)
await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]);
#endif
_servers.Clear();
}
public void Stop()
{
Running = false;
// trigger the event processing loop to end
SetHasEvent();
}
public ILogger GetLogger()
{
return Logger;
}
public IList<MessageToken> GetMessageTokens()
{
return MessageTokens;
}
public IList<Player> GetActiveClients()
{
var ActiveClients = new List<Player>();
foreach (var server in _servers)
ActiveClients.AddRange(server.Players.Where(p => p != null));
return ActiveClients;
}
public ClientService GetClientService() => ClientSvc;
public AliasService GetAliasService() => AliasSvc;
public PenaltyService GetPenaltyService() => PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
public IEventApi GetEventApi() => Api;
public bool ShutdownRequested() => !Running;
public IEventHandler GetEventHandler() => Handler;
public void SetHasEvent()
{
OnEvent.Set();
}
}
}

View File

@ -0,0 +1,67 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation of IResourceQueryHelper
/// query helper that retrieves administered penalties for provided client id
/// </summary>
public class AdministeredPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public AdministeredPenaltyResourceQueryHelper(ILogger<AdministeredPenaltyResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId)
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When);
var penalties = await iqPenalties
.Take(query.Count)
.Select(_penalty => new AdministeredPenaltyResponse()
{
PenaltyId = _penalty.PenaltyId,
Offense = _penalty.Offense,
AutomatedOffense = _penalty.AutomatedOffense,
ClientId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
OffenderClientId = _penalty.Offender.ClientId,
PunisherClientId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PenaltyType = _penalty.Type,
When = _penalty.When,
ExpirationDate = _penalty.Expires,
IsLinked = _penalty.OffenderId != query.ClientId,
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
})
.ToListAsync();
return new ResourceQueryHelperResult<AdministeredPenaltyResponse>
{
// todo: might need to do count at some point
RetrievedResultCount = penalties.Count,
Results = penalties
};
}
}
}

View File

@ -0,0 +1,60 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
public class
ConnectionsResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public ConnectionsResourceQueryHelper(ILogger<ConnectionsResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<ConnectionHistoryResponse>> QueryResource(
ClientPaginationRequest query)
{
_logger.LogDebug("{Class} {@Request}", nameof(ConnectionsResourceQueryHelper), query);
await using var context = _contextFactory.CreateContext(enableTracking: false);
var iqConnections = context.ConnectionHistory.AsNoTracking()
.Where(history => query.ClientId == history.ClientId)
.Where(history => history.CreatedDateTime < query.Before)
.OrderByDescending(history => history.CreatedDateTime);
var connections = await iqConnections.Select(history => new ConnectionHistoryResponse
{
MetaId = history.ClientConnectionId,
ClientId = history.ClientId,
Type = MetaType.ConnectionHistory,
ShouldDisplay = true,
When = history.CreatedDateTime,
ServerName = history.Server.HostName,
ConnectionType = history.ConnectionType
})
.ToListAsync();
_logger.LogDebug("{Class} retrieved {Number} items", nameof(ConnectionsResourceQueryHelper),
connections.Count);
return new ResourceQueryHelperResult<ConnectionHistoryResponse>
{
Results = connections
};
}
}
}

View File

@ -0,0 +1,206 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
public class MetaRegistration : IMetaRegistration
{
private readonly ILogger _logger;
private ITranslationLookup _transLookup;
private readonly IMetaServiceV2 _metaService;
private readonly IEntityService<EFClient> _clientEntityService;
private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>
_administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>
_connectionHistoryHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>
_permissionLevelHelper;
public MetaRegistration(ILogger<MetaRegistration> logger, IMetaServiceV2 metaService,
ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper,
IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse> connectionHistoryHelper,
IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse> permissionLevelHelper)
{
_logger = logger;
_transLookup = transLookup;
_metaService = metaService;
_clientEntityService = clientEntityService;
_receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper;
_connectionHistoryHelper = connectionHistoryHelper;
_permissionLevelHelper = permissionLevelHelper;
}
public void Register()
{
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information,
GetProfileMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty,
GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized,
GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate,
GetUpdatedAliasMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ConnectionHistoryResponse>(MetaType.ConnectionHistory,
GetConnectionHistoryMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, PermissionLevelChangedResponse>(
MetaType.PermissionLevel, GetPermissionLevelMeta);
}
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request,
CancellationToken cancellationToken = default)
{
var metaList = new List<InformationResponse>();
var lastMapMeta =
await _metaService.GetPersistentMeta("LastMapPlayed", request.ClientId, cancellationToken);
if (lastMapMeta != null)
{
metaList.Add(new InformationResponse()
{
ClientId = request.ClientId,
MetaId = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
ShouldDisplay = true,
Type = MetaType.Information,
Order = 6
});
}
var lastServerMeta =
await _metaService.GetPersistentMeta("LastServerPlayed", request.ClientId, cancellationToken);
if (lastServerMeta != null)
{
metaList.Add(new InformationResponse()
{
ClientId = request.ClientId,
MetaId = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
ShouldDisplay = true,
Type = MetaType.Information,
Order = 7
});
}
var client = await _clientEntityService.Get(request.ClientId);
if (client == null)
{
_logger.LogWarning("No client found with id {ClientId} when generating profile meta", request.ClientId);
return metaList;
}
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Order = 8,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Order = 9,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Order = 10,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0",
new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
ShouldDisplay = true,
Order = 11,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
Value = client.Masked
? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"]
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
IsSensitive = true,
Order = 12,
Type = MetaType.Information
});
return metaList;
}
private async Task<IEnumerable<ReceivedPenaltyResponse>> GetReceivedPenaltiesMeta(
ClientPaginationRequest request, CancellationToken token = default)
{
var penalties = await _receivedPenaltyHelper.QueryResource(request);
return penalties.Results;
}
private async Task<IEnumerable<AdministeredPenaltyResponse>> GetAdministeredPenaltiesMeta(
ClientPaginationRequest request, CancellationToken token = default)
{
var penalties = await _administeredPenaltyHelper.QueryResource(request);
return penalties.Results;
}
private async Task<IEnumerable<UpdatedAliasResponse>> GetUpdatedAliasMeta(ClientPaginationRequest request,
CancellationToken token = default)
{
var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results;
}
private async Task<IEnumerable<ConnectionHistoryResponse>> GetConnectionHistoryMeta(
ClientPaginationRequest request, CancellationToken token = default)
{
var connections = await _connectionHistoryHelper.QueryResource(request);
return connections.Results;
}
private async Task<IEnumerable<PermissionLevelChangedResponse>> GetPermissionLevelMeta(
ClientPaginationRequest request, CancellationToken token = default)
{
var permissionChanges = await _permissionLevelHelper.QueryResource(request);
return permissionChanges.Results;
}
}
}

View File

@ -0,0 +1,52 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
namespace IW4MAdmin.Application.Meta;
public class
PermissionLevelChangedResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest,
PermissionLevelChangedResponse>
{
private readonly IDatabaseContextFactory _contextFactory;
public PermissionLevelChangedResourceQueryHelper(IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<PermissionLevelChangedResponse>> QueryResource(
ClientPaginationRequest query)
{
await using var context = _contextFactory.CreateContext();
var auditEntries = context.EFChangeHistory.Where(change => change.TargetEntityId == query.ClientId)
.Where(change => change.TypeOfChange == EFChangeHistory.ChangeType.Permission)
.OrderByDescending(change => change.TimeChanged);
var audits = from change in auditEntries
join client in context.Clients
on change.OriginEntityId equals client.ClientId
select new PermissionLevelChangedResponse
{
ChangedById = change.OriginEntityId,
ChangedByName = client.CurrentAlias.Name,
PreviousPermissionLevelValue = change.PreviousValue,
CurrentPermissionLevelValue = change.CurrentValue,
When = change.TimeChanged,
ClientId = change.TargetEntityId,
IsSensitive = true
};
return new ResourceQueryHelperResult<PermissionLevelChangedResponse>
{
Results = await audits.Skip(query.Offset).Take(query.Count).ToListAsync()
};
}
}

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
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
{
/// <summary>
/// implementation of IResourceQueryHelper
/// used to pull in penalties applied to a given client id
/// </summary>
public class
ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{
_contextFactory = contextFactory;
_logger = logger;
_appConfig = appConfig;
}
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();
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)
.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();
iqIpLinkedPenalties = ctx.Penalties.AsNoTracking()
.Where(penalty =>
linkedPenaltyType.Contains(penalty.Type) && aliasedIds.Contains(penalty.LinkId ?? -1));
}
var iqAllPenalties = iqPenalties;
if (iqIpLinkedPenalties != null)
{
iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties);
}
if (identifierPenalties != null)
{
iqAllPenalties = iqPenalties.Union(identifierPenalties);
}
var penalties = await iqAllPenalties
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When)
.Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse()
{
PenaltyId = _penalty.PenaltyId,
ClientId = query.ClientId,
Offense = _penalty.Offense,
AutomatedOffense = _penalty.AutomatedOffense,
OffenderClientId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
PunisherClientId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PenaltyType = _penalty.Type,
When = _penalty.When,
ExpirationDate = _penalty.Expires,
IsLinked = _penalty.OffenderId != query.ClientId,
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
})
.ToListAsync();
return new ResourceQueryHelperResult<ReceivedPenaltyResponse>
{
// todo: maybe actually count
RetrievedResultCount = penalties.Count,
Results = penalties.Distinct()
};
}
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation if IResrouceQueryHerlp
/// used to pull alias changes for given client id
/// </summary>
public class UpdatedAliasResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public UpdatedAliasResourceQueryHelper(ILogger<UpdatedAliasResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
{
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases
.Where(_alias => _alias.LinkId == linkId)
.Where(_alias => _alias.DateAdded < query.Before)
.Where(_alias => _alias.IPAddress != null)
.OrderByDescending(_alias => _alias.DateAdded)
.Select(_alias => new UpdatedAliasResponse
{
MetaId = _alias.AliasId,
Name = _alias.Name,
IPAddress = _alias.IPAddress.ConvertIPtoString(),
When = _alias.DateAdded,
Type = MetaType.AliasUpdate,
IsSensitive = true
});
var result = (await iqAliasUpdates
.Take(query.Count)
.ToListAsync())
.Distinct();
return new ResourceQueryHelperResult<UpdatedAliasResponse>
{
Results = result, // we can potentially have duplicates
RetrievedResultCount = result.Count()
};
}
}
}

View File

@ -0,0 +1,104 @@
using SharedLibraryCore;
using System;
using System.IO;
using System.Linq;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Migration
{
/// <summary>
/// helps facilitate the migration of configs from one version and or location
/// to another
/// </summary>
class ConfigurationMigration
{
/// <summary>
/// ensures required directories are created
/// </summary>
public static void CheckDirectories()
{
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Localization")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Localization"));
}
}
/// <summary>
/// moves existing configs from the root folder into a configs folder
/// </summary>
public static void MoveConfigFolder10518(ILogger log)
{
string currentDirectory = Utilities.OperatingDirectory;
// we don't want to do this for migrations or tests where the
// property isn't initialized or it's wrong
if (currentDirectory != null)
{
string configDirectory = Path.Join(currentDirectory, "Configuration");
if (!Directory.Exists(configDirectory))
{
Directory.CreateDirectory(configDirectory);
}
var configurationFiles = Directory.EnumerateFiles(currentDirectory, "*.json")
.Select(f => f.Split(Path.DirectorySeparatorChar).Last())
.Where(f => f.Count(c => c == '.') == 1);
foreach (var configFile in configurationFiles)
{
string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath))
{
File.Move(configFile, destinationPath);
}
}
if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db"))
{
File.Move("Database.db", Path.Join("Database", "Database.db"));
}
}
}
public static void ModifyLogPath020919(SharedLibraryCore.Configuration.ServerConfiguration config)
{
if (config.ManualLogPath.IsRemoteLog())
{
config.GameLogServerUrl = new Uri(config.ManualLogPath);
config.ManualLogPath = null;
}
}
public static void RemoveObsoletePlugins20210322()
{
var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll", "IW4ScriptCommands.dll"};
foreach (var file in files)
{
var path = Path.Join(Utilities.OperatingDirectory, "Plugins", file);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client.Stats;
namespace IW4MAdmin.Application.Migration
{
public static class DatabaseHousekeeping
{
private static readonly DateTime CutoffDate = DateTime.UtcNow.AddMonths(-6);
public static async Task RemoveOldRatings(IDatabaseContextFactory contextFactory, CancellationToken token)
{
await using var context = contextFactory.CreateContext();
var dbSet = context.Set<EFRating>();
var itemsToDelete = dbSet.Where(rating => rating.When <= CutoffDate);
dbSet.RemoveRange(itemsToDelete);
await context.SaveChangesAsync(token);
}
}
}

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

@ -0,0 +1,110 @@
using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// default implementation of IConfigurationHandler
/// </summary>
/// <typeparam name="T">base configuration type</typeparam>
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration
{
private T _configuration;
private readonly SemaphoreSlim _onSaving;
private readonly JsonSerializerOptions _serializerOptions;
public BaseConfigurationHandler(string fileName)
{
_serializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
_serializerOptions.Converters.Add(new JsonStringEnumConverter());
_onSaving = new SemaphoreSlim(1, 1);
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fileName}.json");
}
public BaseConfigurationHandler() : this(typeof(T).Name)
{
}
~BaseConfigurationHandler()
{
_onSaving.Dispose();
}
public string FileName { get; }
public async Task BuildAsync()
{
try
{
await _onSaving.WaitAsync();
await using var fileStream = File.OpenRead(FileName);
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
}
catch (FileNotFoundException)
{
_configuration = default;
}
catch (Exception e)
{
throw new ConfigurationException("Could not load configuration")
{
Errors = new[] { e.Message },
ConfigurationFileName = FileName
};
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
}
public async Task Save()
{
try
{
await _onSaving.WaitAsync();
await using var fileStream = File.Create(FileName);
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
await fileStream.DisposeAsync();
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
}
public T Configuration()
{
return _configuration;
}
public void Set(T config)
{
_configuration = config;
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Data.Models;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IClientNoticeMessageFormatter
/// </summary>
public class ClientNoticeMessageFormatter : IClientNoticeMessageFormatter
{
private readonly ITranslationLookup _transLookup;
private readonly ApplicationConfiguration _appConfig;
public ClientNoticeMessageFormatter(ITranslationLookup transLookup, ApplicationConfiguration appConfig)
{
_transLookup = transLookup;
_appConfig = appConfig;
}
public string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null)
{
var isNewLineSeparator = config.NoticeLineSeparator == Environment.NewLine;
var penalty = originalPenalty ?? currentPenalty;
var builder = new StringBuilder();
// build the top level header
var header = _transLookup[$"SERVER_{penalty.Type.ToString().ToUpper()}_TEXT"];
builder.Append(header);
builder.Append(config.NoticeLineSeparator);
// build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
if (isNewLineSeparator)
{
foreach (var splitReason in SplitOverMaxLength(reason, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(config.NoticeLineSeparator);
}
}
else
{
builder.Append(reason);
builder.Append(config.NoticeLineSeparator);
}
if (penalty.Type == EFPenalty.PenaltyType.TempBan)
{
// build the time remaining if temporary
var timeRemainingValue = penalty.Expires.HasValue
? (penalty.Expires - DateTime.UtcNow).Value.HumanizeForCurrentCulture()
: "--";
var timeRemaining = _transLookup["GAME_MESSAGE_PENALTY_TIME_REMAINING"].FormatExt(timeRemainingValue);
if (isNewLineSeparator)
{
foreach (var splitReason in SplitOverMaxLength(timeRemaining, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(config.NoticeLineSeparator);
}
}
else
{
builder.Append(timeRemaining);
}
}
if (penalty.Type == EFPenalty.PenaltyType.Ban)
{
// provide a place to appeal the ban (should always be specified but including a placeholder just incase)
builder.Append(_transLookup["GAME_MESSAGE_PENALTY_APPEAL"].FormatExt(_appConfig.ContactUri ?? "--"));
}
// final format looks something like:
/*
* You are permanently banned
* Reason - toxic behavior
* Visit example.com to appeal
*/
return builder.ToString();
}
private static IEnumerable<string> SplitOverMaxLength(string source, int maxCharactersPerLine)
{
if (source.Length <= maxCharactersPerLine)
{
return new[] {source};
}
var segments = new List<string>();
var currentLocation = 0;
while (currentLocation < source.Length)
{
var nextLocation = currentLocation + maxCharactersPerLine;
// there's probably a more efficient way to do this but this is readable
segments.Add(string.Concat(
source
.Skip(currentLocation)
.Take(Math.Min(maxCharactersPerLine, source.Length - currentLocation))));
currentLocation = nextLocation;
}
if (currentLocation < source.Length)
{
segments.Add(source.Substring(currentLocation, source.Length - currentLocation));
}
return segments;
}
}
}

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);
country = reader.Country(address);
}
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,171 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using InteractionRegistrationCallback =
System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken,
System.Threading.Tasks.Task<SharedLibraryCore.Interfaces.IInteractionData>>;
namespace IW4MAdmin.Application.Misc;
public class InteractionRegistration : IInteractionRegistration
{
private readonly ILogger<InteractionRegistration> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<string, InteractionRegistrationCallback> _interactions = new();
public InteractionRegistration(ILogger<InteractionRegistration> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration)
{
if (string.IsNullOrWhiteSpace(source))
{
throw new ArgumentException("Script interaction source cannot be null");
}
_logger.LogDebug("Registering script interaction {InteractionName} from {Source}", interactionName, source);
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
.FirstOrDefault(plugin => plugin.Name == source);
if (plugin is not ScriptPlugin scriptPlugin)
{
return;
}
Task<IInteractionData> WrappedDelegate(int? clientId, Reference.Game? game, CancellationToken token) =>
Task.FromResult(
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, token, clientId, game, token));
if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, WrappedDelegate);
}
else
{
_interactions[interactionName] = WrappedDelegate;
}
}
public void RegisterInteraction(string interactionName, InteractionRegistrationCallback interactionRegistration)
{
if (!_interactions.ContainsKey(interactionName))
{
_logger.LogDebug("Registering interaction {InteractionName}", interactionName);
_interactions.TryAdd(interactionName, interactionRegistration);
}
else
{
_logger.LogDebug("Updating interaction {InteractionName}", interactionName);
_interactions[interactionName] = interactionRegistration;
}
}
public void UnregisterInteraction(string interactionName)
{
if (!_interactions.ContainsKey(interactionName))
{
return;
}
_logger.LogDebug("Unregistering interaction {InteractionName}", interactionName);
_interactions.TryRemove(interactionName, out _);
}
public async Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null,
int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return await GetInteractionsInternal(interactionPrefix, clientId, game, token);
}
public async Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null,
Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default)
{
if (!_interactions.ContainsKey(interactionId))
{
throw new ArgumentException($"Interaction with ID {interactionId} has not been registered");
}
try
{
var interaction = await _interactions[interactionId](targetId, game, token);
if (interaction.Action is not null)
{
return await interaction.Action(originId, targetId, game, meta, token);
}
if (interaction.ScriptAction is not null)
{
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>())
{
if (plugin is not ScriptPlugin scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, token, originId, targetId, game, meta,
token);
}
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPluginV2>>())
{
if (plugin is not ScriptPluginV2 scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}
return scriptPlugin
.QueryWithErrorHandling(interaction.ScriptAction, originId, targetId, game, meta, token)
?.ToString();
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not process interaction for {InteractionName} and OriginId {ClientId}",
interactionId, originId);
}
return null;
}
private async Task<IEnumerable<IInteractionData>> GetInteractionsInternal(string prefix = null,
int? clientId = null, Reference.Game? game = null, CancellationToken token = default)
{
var interactions = _interactions
.Where(interaction => string.IsNullOrWhiteSpace(prefix) || interaction.Key.StartsWith(prefix)).Select(
async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for {InteractionName} and ClientId {ClientId}",
kvp.Key,
clientId);
return null;
}
});
return (await Task.WhenAll(interactions))
.Where(interaction => interaction is not null)
.ToList();
}
}

View File

@ -0,0 +1,57 @@
using System.Runtime.InteropServices;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// dto class for handling log path generation
/// </summary>
public class LogPathGeneratorInfo
{
/// <summary>
/// directory under the paths where data comes from by default
/// <remarks>fs_basegame</remarks>
/// </summary>
public string BaseGameDirectory { get; set; } = "";
/// <summary>
/// base game root path
/// <remarks>fs_basepath</remarks>
/// </summary>
public string BasePathDirectory { get; set; } = "";
/// <summary>
/// directory for local storage
/// <remarks>fs_homepath</remarks>
/// </summary>
public string HomePathDirectory { get; set; } = "";
/// <summary>
/// overide game directory
/// <remarks>plugin driven</remarks>
/// </summary>
public string GameDirectory { get; set; } = "";
/// <summary>
/// game director
/// <remarks>fs_game</remarks>
/// </summary>
public string ModDirectory { get; set; } = "";
/// <summary>
/// log file name
/// <remarks>g_log</remarks>
/// </summary>
public string LogFile { get; set; } = "";
/// <summary>
/// indicates if running on windows
/// </summary>
public bool IsWindows { get; set; } = true;
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
public bool IsOneLog { get; set; }
}
}

View File

@ -0,0 +1,47 @@
using System;
using Microsoft.Extensions.Logging;
using ILogger = SharedLibraryCore.Interfaces.ILogger;
namespace IW4MAdmin.Application
{
[Obsolete]
public class Logger : ILogger
{
private readonly Microsoft.Extensions.Logging.ILogger _logger;
public Logger(ILogger<Logger> logger)
{
_logger = logger;
}
public void WriteVerbose(string msg)
{
_logger.LogInformation(msg);
}
public void WriteDebug(string msg)
{
_logger.LogDebug(msg);
}
public void WriteError(string msg)
{
_logger.LogError(msg);
}
public void WriteInfo(string msg)
{
WriteVerbose(msg);
}
public void WriteWarning(string msg)
{
_logger.LogWarning(msg);
}
public void WriteAssert(bool condition, string msg)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,175 @@
using IW4MAdmin.Application.API.Master;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IMasterCommunication
/// talks to the master server
/// </summary>
class MasterCommunication : IMasterCommunication
{
private readonly ILogger _logger;
private readonly ITranslationLookup _transLookup;
private readonly IMasterApi _apiInstance;
private readonly IManager _manager;
private readonly ApplicationConfiguration _appConfig;
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private readonly int _apiVersion = 1;
private bool _firstHeartBeat = true;
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{
_logger = logger;
_transLookup = translationLookup;
_apiInstance = apiInstance;
_appConfig = appConfig;
_manager = manager;
}
/// <summary>
/// checks for latest version of the application
/// notifies user if an update is available
/// </summary>
/// <returns></returns>
public async Task CheckVersion()
{
var version = new VersionInfo()
{
CurrentVersionStable = _fallbackVersion
};
try
{
version = await _apiInstance.GetVersion(_apiVersion);
}
catch (Exception e)
{
_logger.LogWarning(e, "Unable to retrieve IW4MAdmin version information");
}
if (version.CurrentVersionStable == _fallbackVersion)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(_transLookup["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Program.Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {_transLookup["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString()}]");
Console.WriteLine(_transLookup["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Program.Version.ToString()}]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Program.Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {_transLookup["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString()}-pr]");
Console.WriteLine(_transLookup["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Program.Version.ToString()}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(_transLookup["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
public async Task RunUploadStatus(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (_manager.IsRunning)
{
await UploadStatus();
}
}
catch (Exception ex)
{
_logger.LogWarning("Could not send heartbeat - {Message}", ex.Message);
}
try
{
await Task.Delay(Interval, token);
}
catch
{
break;
}
}
}
private async Task UploadStatus()
{
if (_firstHeartBeat)
{
var token = await _apiInstance.Authenticate(new AuthenticationId
{
Id = _appConfig.Id
});
_apiInstance.AuthorizationToken = $"Bearer {token.AccessToken}";
}
var instance = new ApiInstance
{
Id = _appConfig.Id,
Uptime = (int)(DateTime.UtcNow - (_manager as ApplicationManager).StartTime).TotalSeconds,
Version = Program.Version,
Servers = _manager.GetServers().Select(s =>
new ApiServer()
{
ClientNum = s.ClientNum,
Game = s.GameName.ToString(),
Version = s.Version,
Gametype = s.Gametype,
Hostname = s.Hostname,
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.EndPoint,
Port = (short)s.ListenPort,
IPAddress = s.ListenAddress
}).ToList(),
WebfrontUrl = _appConfig.WebfrontUrl
};
Response<ResultMessage> response;
if (_firstHeartBeat)
{
response = await _apiInstance.AddInstance(instance);
}
else
{
response = await _apiInstance.UpdateInstance(instance.Id, instance);
_firstHeartBeat = false;
}
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.ResponseMessage.StatusCode, response.StringContent);
}
}
}
}

View File

@ -0,0 +1,312 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Models;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IMetaService
/// used to add and retrieve runtime and persistent meta
/// </summary>
[Obsolete("Use MetaServiceV2")]
public class MetaService : IMetaService
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public MetaService(ILogger<MetaService> logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
}
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client, EFMeta linkedMeta = null)
{
// this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1)
{
return;
}
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
.Where(_meta => _meta.ClientId == client.ClientId)
.FirstOrDefaultAsync();
if (existingMeta != null)
{
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
existingMeta.LinkedMetaId = linkedMeta?.MetaId;
}
else
{
ctx.EFMeta.Add(new EFMeta()
{
ClientId = client.ClientId,
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue,
LinkedMetaId = linkedMeta?.MetaId
});
}
await ctx.SaveChangesAsync();
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId)
{
await AddPersistentMeta(metaKey, metaValue, new EFClient { ClientId = clientId });
}
public async Task IncrementPersistentMeta(string metaKey, int incrementAmount, int clientId)
{
var existingMeta = await GetPersistentMeta(metaKey, new EFClient { ClientId = clientId });
if (!long.TryParse(existingMeta?.Value ?? "0", out var existingValue))
{
existingValue = 0;
}
var newValue = existingValue + incrementAmount;
await SetPersistentMeta(metaKey, newValue.ToString(), clientId);
}
public async Task DecrementPersistentMeta(string metaKey, int decrementAmount, int clientId)
{
await IncrementPersistentMeta(metaKey, -decrementAmount, clientId);
}
public async Task AddPersistentMeta(string metaKey, string metaValue)
{
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
var matchValues = existingMeta
.Where(meta => meta.Value == metaValue)
.ToArray();
if (matchValues.Any())
{
foreach (var meta in matchValues)
{
_logger.LogDebug("Updating existing meta with key {key} and id {id}", meta.Key, meta.MetaId);
meta.Value = metaValue;
meta.Updated = DateTime.UtcNow;
}
await ctx.SaveChangesAsync();
}
else
{
_logger.LogDebug("Adding new meta with key {key}", metaKey);
ctx.EFMeta.Add(new EFMeta()
{
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
await ctx.SaveChangesAsync();
}
}
public async Task RemovePersistentMeta(string metaKey, EFClient client)
{
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == client.ClientId);
if (existingMeta == null)
{
_logger.LogDebug("No meta with key {key} found for client id {id}", metaKey, client.ClientId);
return;
}
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, existingMeta.MetaId);
context.EFMeta.Remove(existingMeta);
await context.SaveChangesAsync();
}
public async Task RemovePersistentMeta(string metaKey, string metaValue = null)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
if (metaValue == null)
{
_logger.LogDebug("Removing all meta for key {key} with ids [{ids}] ", metaKey, string.Join(", ", existingMeta.Select(meta => meta.MetaId)));
existingMeta.ForEach(meta => context.Remove(existingMeta));
await context.SaveChangesAsync();
return;
}
var foundMeta = existingMeta.FirstOrDefault(meta => meta.Value == metaValue);
if (foundMeta != null)
{
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, foundMeta.MetaId);
context.Remove(foundMeta);
await context.SaveChangesAsync();
}
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
.Where(_meta => _meta.ClientId == client.ClientId)
.Select(_meta => new EFMeta()
{
MetaId = _meta.MetaId,
Key = _meta.Key,
ClientId = _meta.ClientId,
Value = _meta.Value,
LinkedMetaId = _meta.LinkedMetaId,
LinkedMeta = _meta.LinkedMetaId != null ? new EFMeta()
{
MetaId = _meta.LinkedMeta.MetaId,
Key = _meta.LinkedMeta.Key,
Value = _meta.LinkedMeta.Value
} : null
})
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<EFMeta>> GetPersistentMeta(string metaKey)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.Select(meta => new EFMeta
{
MetaId = meta.MetaId,
Key = meta.Key,
ClientId = meta.ClientId,
Value = meta.Value,
})
.ToListAsync();
}
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
{
if (!_metaActions.ContainsKey(metaKey))
{
_metaActions.Add(metaKey, new List<dynamic>() { metaAction });
}
else
{
_metaActions[metaKey].Add(metaAction);
}
}
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
{
var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information)
.Select(async kvp => await kvp.Value[0](request)));
return metas.SelectMany(m => (IEnumerable<IClientMeta>)m)
.OrderByDescending(m => m.When)
.Take(request.Count)
.ToList();
}
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
{
if (metaType == MetaType.Information)
{
var allMeta = new List<T>();
var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration =>
(IEnumerable<T>)await individualMetaRegistration(request)));
allMeta.AddRange(completedMeta.SelectMany(meta => meta));
return ProcessInformationMeta(allMeta);
}
var meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
return meta;
}
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
{
var table = new List<List<T>>();
var metaWithColumn = meta
.Where(_meta => _meta.Column != null);
var columnGrouping = metaWithColumn
.GroupBy(_meta => _meta.Column);
var metaToSort = meta.Except(metaWithColumn).ToList();
foreach (var metaItem in columnGrouping)
{
table.Add(new List<T>(metaItem));
}
while (metaToSort.Count > 0)
{
var sortingMeta = metaToSort.First();
int indexOfSmallestColumn()
{
int index = 0;
int smallestColumnSize = int.MaxValue;
for (int i = 0; i < table.Count; i++)
{
if (table[i].Count < smallestColumnSize)
{
smallestColumnSize = table[i].Count;
index = i;
}
}
return index;
}
int columnIndex = indexOfSmallestColumn();
sortingMeta.Column = columnIndex;
sortingMeta.Order = columnGrouping
.First(_group => _group.Key == columnIndex)
.Count();
table[columnIndex].Add(sortingMeta);
metaToSort.Remove(sortingMeta);
}
return meta;
}
}
}

View File

@ -0,0 +1,453 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc;
public class MetaServiceV2 : IMetaServiceV2
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == clientId)
.FirstOrDefaultAsync(token);
if (existingMeta != null)
{
_logger.LogDebug("Updating existing meta with key {Key} and id {Id}", existingMeta.Key,
existingMeta.MetaId);
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
}
else
{
_logger.LogDebug("Adding new meta with key {Key}", metaKey);
context.EFMeta.Add(new EFMeta
{
ClientId = clientId,
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue,
});
}
await context.SaveChangesAsync(token);
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,
CancellationToken token = default) where T : class
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
string serializedValue;
try
{
serializedValue = JsonSerializer.Serialize(metaValue);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not serialize meta with key {Key}", metaKey);
return;
}
await SetPersistentMeta(metaKey, serializedValue, clientId, token);
}
public async Task SetPersistentMetaForLookupKey(string metaKey, string lookupKey, int lookupId, int clientId,
CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
await using var context = _contextFactory.CreateContext();
var lookupMeta = await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == lookupKey, token);
if (lookupMeta is null)
{
_logger.LogWarning("No lookup meta exists for metaKey {MetaKey} and lookupKey {LookupKey}", metaKey,
lookupKey);
return;
}
var lookupValues = JsonSerializer.Deserialize<List<LookupValue<string>>>(lookupMeta.Value);
if (lookupValues is null)
{
return;
}
var foundLookup = lookupValues.FirstOrDefault(value => value.Id == lookupId);
if (foundLookup is null)
{
_logger.LogWarning("No lookup meta found for provided lookup id {MetaKey}, {LookupKey}, {LookupId}",
metaKey, lookupKey, lookupId);
return;
}
_logger.LogDebug("Setting meta for lookup {MetaKey}, {LookupKey}, {LookupId}",
metaKey, lookupKey, lookupId);
await SetPersistentMeta(metaKey, lookupId.ToString(), clientId, token);
}
public async Task IncrementPersistentMeta(string metaKey, int incrementAmount, int clientId,
CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
var existingMeta = await GetPersistentMeta(metaKey, clientId, token);
if (!long.TryParse(existingMeta?.Value ?? "0", out var existingValue))
{
existingValue = 0;
}
var newValue = existingValue + incrementAmount;
await SetPersistentMeta(metaKey, newValue.ToString(), clientId, token);
}
public async Task DecrementPersistentMeta(string metaKey, int decrementAmount, int clientId,
CancellationToken token = default)
{
await IncrementPersistentMeta(metaKey, -decrementAmount, clientId, token);
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, int clientId, CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return null;
}
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == clientId)
.Select(meta => new EFMeta
{
MetaId = meta.MetaId,
Key = meta.Key,
ClientId = meta.ClientId,
Value = meta.Value,
})
.FirstOrDefaultAsync(token);
}
public async Task<T> GetPersistentMetaValue<T>(string metaKey, int clientId, CancellationToken token = default)
where T : class
{
var meta = await GetPersistentMeta(metaKey, clientId, token);
if (meta is null)
{
return default;
}
try
{
return JsonSerializer.Deserialize<T>(meta.Value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not deserialize meta with key {Key} and value {Value}", metaKey, meta.Value);
return default;
}
}
public async Task<EFMeta> GetPersistentMetaByLookup(string metaKey, string lookupKey, int clientId,
CancellationToken token = default)
{
await using var context = _contextFactory.CreateContext();
var metaValue = await GetPersistentMeta(metaKey, clientId, token);
if (metaValue is null)
{
_logger.LogDebug("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId);
return default;
}
var lookupMeta = await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == lookupKey, token);
if (lookupMeta is null)
{
_logger.LogWarning("No lookup meta exists for metaKey {MetaKey} and lookupKey {LookupKey}", metaKey,
lookupKey);
return default;
}
var lookupId = int.Parse(metaValue.Value);
var lookupValues = JsonSerializer.Deserialize<List<LookupValue<string>>>(lookupMeta.Value);
if (lookupValues is null)
{
return default;
}
var foundLookup = lookupValues.FirstOrDefault(value => value.Id == lookupId);
if (foundLookup is not null)
{
return new EFMeta
{
Created = metaValue.Created,
Updated = metaValue.Updated,
Extra = metaValue.Extra,
MetaId = metaValue.MetaId,
Value = foundLookup.Value
};
}
_logger.LogWarning("No lookup meta found for provided lookup id {MetaKey}, {LookupKey}, {LookupId}",
metaKey, lookupKey, lookupId);
return default;
}
public async Task RemovePersistentMeta(string metaKey, int clientId, CancellationToken token = default)
{
if (!ValidArgs(metaKey, clientId))
{
return;
}
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == clientId, token);
if (existingMeta == null)
{
_logger.LogDebug("No meta with key {Key} found for client id {Id}", metaKey, clientId);
return;
}
_logger.LogDebug("Removing meta for key {Key} with id {Id}", metaKey, existingMeta.MetaId);
context.EFMeta.Remove(existingMeta);
await context.SaveChangesAsync(token);
}
public async Task SetPersistentMeta(string metaKey, string metaValue, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(metaKey))
{
_logger.LogWarning("Cannot save meta with no key");
return;
}
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.FirstOrDefaultAsync(token);
if (existingMeta is not null)
{
_logger.LogDebug("Updating existing meta with key {Key} and id {Id}", existingMeta.Key,
existingMeta.MetaId);
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
await ctx.SaveChangesAsync(token);
}
else
{
_logger.LogDebug("Adding new meta with key {Key}", metaKey);
ctx.EFMeta.Add(new EFMeta
{
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
await ctx.SaveChangesAsync(token);
}
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, CancellationToken token = default)
where T : class
{
if (string.IsNullOrWhiteSpace(metaKey))
{
_logger.LogWarning("Meta key is null, not setting");
return;
}
if (metaValue is null)
{
_logger.LogWarning("Meta value is null, not setting");
return;
}
string serializedMeta;
try
{
serializedMeta = JsonSerializer.Serialize(metaValue);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not serialize meta with {Key} and value {Value}", metaKey, metaValue);
return;
}
await SetPersistentMeta(metaKey, serializedMeta, token);
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(metaKey))
{
return null;
}
await using var context = _contextFactory.CreateContext(false);
return await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == metaKey, token);
}
public async Task<T> GetPersistentMetaValue<T>(string metaKey, CancellationToken token = default) where T : class
{
if (string.IsNullOrWhiteSpace(metaKey))
{
return default;
}
var meta = await GetPersistentMeta(metaKey, token);
if (meta is null)
{
return default;
}
try
{
return JsonSerializer.Deserialize<T>(meta.Value);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not serialize meta with key {Key} and value {Value}", metaKey, meta.Value);
return default;
}
}
public async Task RemovePersistentMeta(string metaKey, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(metaKey))
{
return;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.FirstOrDefaultAsync(token);
if (existingMeta != null)
{
_logger.LogDebug("Removing meta for key {Key} with id {Id}", metaKey, existingMeta.MetaId);
context.Remove(existingMeta);
await context.SaveChangesAsync(token);
}
}
public void AddRuntimeMeta<T, TReturnType>(MetaType metaKey,
Func<T, CancellationToken, Task<IEnumerable<TReturnType>>> metaAction)
where T : PaginationRequest where TReturnType : IClientMeta
{
if (!_metaActions.ContainsKey(metaKey))
{
_metaActions.Add(metaKey, new List<dynamic> { metaAction });
}
else
{
_metaActions[metaKey].Add(metaAction);
}
}
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request, CancellationToken token = default)
{
var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information)
.Select(async kvp => await kvp.Value[0](request, token)));
return metas.SelectMany(m => (IEnumerable<IClientMeta>)m)
.OrderByDescending(m => m.When)
.Take(request.Count)
.ToList();
}
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType, CancellationToken token = default)
where T : IClientMeta
{
if (metaType == MetaType.Information)
{
var allMeta = new List<T>();
var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration =>
(IEnumerable<T>)await individualMetaRegistration(request, token)));
allMeta.AddRange(completedMeta.SelectMany(meta => meta));
return ProcessInformationMeta(allMeta);
}
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
{
return meta;
}
private static bool ValidArgs(string key, int clientId) => !string.IsNullOrWhiteSpace(key) && clientId > 0;
}

View File

@ -0,0 +1,74 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
class MiddlewareActionHandler : IMiddlewareActionHandler
{
private readonly IDictionary<string, IList<object>> _actions;
private readonly ILogger _logger;
public MiddlewareActionHandler(ILogger<MiddlewareActionHandler> logger)
{
_actions = new Dictionary<string, IList<object>>();
_logger = logger;
}
/// <summary>
/// Executes the action with the given name
/// </summary>
/// <typeparam name="T">Execution return type</typeparam>
/// <param name="value">Input value</param>
/// <param name="name">Name of action to execute</param>
/// <returns></returns>
public async Task<T> Execute<T>(T value, string name = null)
{
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
if (_actions.ContainsKey(key))
{
foreach (var action in _actions[key])
{
try
{
value = await ((IMiddlewareAction<T>)action).Invoke(value);
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to invoke middleware action {name}", name);
}
}
return value;
}
return value;
}
/// <summary>
/// Registers an action by name
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="actionType">Action type specifier</param>
/// <param name="action">Action to perform</param>
/// <param name="name">Name of action</param>
public void Register<T>(T actionType, IMiddlewareAction<T> action, string name = null)
{
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
if (_actions.ContainsKey(key))
{
_actions[key].Add(action);
}
else
{
_actions.Add(key, new[] { action });
}
}
}
}

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