diff --git a/.github/workflows/build_application.yml b/.github/workflows/build_application.yml new file mode 100644 index 00000000..c9aa6779 --- /dev/null +++ b/.github/workflows/build_application.yml @@ -0,0 +1,257 @@ +name: Application build + +on: + push: + branches: [ develop, release/pre, master ] + paths: + - Application/** + - WebfrontCore/** + - Data/** + - SharedLibraryCore/** + - Plugins/** + pull_request: + branches: [ develop ] + paths: + - Application/** + - WebfrontCore/** + - Data/** + - SharedLibraryCore/** + - Plugins/** + +env: + releaseType: prerelease + +jobs: + update_revision_number: + runs-on: ubuntu-latest + + outputs: + revision_number: ${{ steps.revision.outputs.revision_number }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Restore cache + id: cache + uses: actions/cache@v4 + with: + path: cache_dir + key: revision-number + + - name: Get current date + id: date + run: echo "current_date=$(date +'%Y-%m-%d')" >> $GITHUB_ENV + + - name: Check and update revision number + id: revision + run: | + FILENAME=cache_dir/revision_number.txt + DATEFILE=cache_dir/previous_date.txt + + mkdir -p cache_dir + + if [ -f "$DATEFILE" ]; then + prev_date=$(cat "$DATEFILE") + rev_number=$(cat "$FILENAME") + else + prev_date="" + rev_number=0 + fi + + if [ "$current_date" = "$prev_date" ]; then + rev_number=$((rev_number + 1)) + else + rev_number=1 + fi + + echo "New revision number: $rev_number" + echo $rev_number > "$FILENAME" + echo $current_date > "$DATEFILE" + echo "revision_number=$rev_number" >> $GITHUB_OUTPUT + + - name: Save cache + uses: actions/cache@v4 + with: + path: cache_dir + key: revision-number + + make_version: + runs-on: ubuntu-latest + needs: [ update_revision_number ] + + outputs: + build_num: ${{ steps.generate_build_number.outputs.build_num }} + env: + revisionNumber: ${{ needs.update_revision_number.outputs.revision_number }} + + steps: + - name: Make build number + id: generate_build_number + run: | + build_num=$(date +'%Y.%-m.%-d').${{ env.revisionNumber }} + echo "build_num=$build_num" >> $GITHUB_OUTPUT + echo "Build number is $build_num" + + build: + runs-on: ubuntu-latest + needs: [ make_version ] + + env: + solution: IW4MAdmin.sln + buildConfiguration: Prerelease + isPreRelease: false + + buildPlatform: Any CPU + outputFolder: ${{ github.workspace }}/Publish/Prerelease + buildNumber: ${{ needs.make_version.outputs.build_num }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore NuGet packages + run: dotnet restore ${{ env.solution }} + + - name: Preload external resources + run: | + echo "Build Configuration is ${{ env.buildConfiguration }}, Release Type is ${{ env.releaseType }}" + mkdir -p WebfrontCore/wwwroot/lib/open-iconic/font/css + curl -o WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss + sed -i 's#../fonts/#/font/#g' WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss + + - name: Build webfront + run: dotnet build WebfrontCore/WebfrontCore.csproj -c ${{ env.buildConfiguration }} /p:Configuration=${{ env.buildConfiguration }} /p:Platform="x64" + + - name: Compile SCSS files + run: | + dotnet tool install Excubo.WebCompiler --global + webcompiler -r WebfrontCore/wwwroot/css/src -o WebfrontCore/wwwroot/css/ -m disable -z disable + webcompiler WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss -o WebfrontCore/wwwroot/css/ -m disable -z disable + + - name: Bundle JS files + run: | + echo 'Getting dotnet bundle' + curl -o ${{ github.workspace }}/dotnet-bundle.zip https://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip + echo 'Unzipping download' + unzip ${{ github.workspace }}/dotnet-bundle.zip -d ${{ github.workspace }}/bundle + echo 'Executing dotnet-bundle' + cd ${{ github.workspace }}/bundle + dotnet dotnet-bundle.dll clean ${{ github.workspace }}/WebfrontCore/bundleconfig.json + dotnet dotnet-bundle.dll ${{ github.workspace }}/WebfrontCore/bundleconfig.json + + - name: Build plugins + run: | + cd Plugins + find . -name "*.csproj" -print0 | xargs -0 -I {} dotnet publish {} -c ${{ env.buildConfiguration }} -o ../BUILD/Plugins /p:Configuration=${{ env.buildConfiguration }} /p:Platform="x64" /p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:Version=${{ env.buildNumber }} --no-restore + + - name: Build application + run: dotnet publish Application/Application.csproj -c ${{ env.buildConfiguration }} -o ${{ env.outputFolder }} /p:Version=${{ env.buildNumber }} /p:Configuration=${{ env.buildConfiguration }} /p:Platform="x64" --no-restore + + - name: Download translations + run: | + mkdir -p "${{ env.outputFolder }}/Localization" + localizations=("en-US" "ru-RU" "es-EC" "pt-BR" "de-DE") + for localization in "${localizations[@]}" + do + url="https://master.iw4.zip/localization/$localization" + filePath="${{ env.outputFolder }}/Localization/IW4MAdmin.$localization.json" + curl -s "$url" -o "$filePath" + done + + - name: Clean up publish files + run: | + chmod +x ${{ github.workspace }}/Application/BuildScripts/PostBuild.sh + bash ${{ github.workspace }}/Application/BuildScripts/PostBuild.sh ${{ env.outputFolder }} ${{ github.workspace }} + + - name: Generate start scripts + run: | + cat << EOF > "${{ env.outputFolder }}/StartIW4MAdmin.cmd" + @echo off + @title IW4MAdmin + set DOTNET_CLI_TELEMETRY_OPTOUT=1 + dotnet Lib\IW4MAdmin.dll + pause + EOF + + cat << EOF > "${{ env.outputFolder }}/StartIW4MAdmin.sh" + #!/bin/bash + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + dotnet Lib/IW4MAdmin.dll + EOF + + - name: Move extra content into publish directory + run: | + cp ${{ github.workspace }}/Plugins/ScriptPlugins/*.js ${{ env.outputFolder }}/Plugins/ + cp ${{ github.workspace }}/BUILD/Plugins/*.dll ${{ env.outputFolder }}/Plugins/ + mkdir -p ${{ env.outputFolder }}/wwwroot/css + cp ${{ github.workspace }}/WebfrontCore/wwwroot/css/global.min.css ${{ env.outputFolder }}/wwwroot/css/global.min.css + mkdir -p ${{ env.outputFolder }}/wwwroot/js + cp ${{ github.workspace }}/WebfrontCore/wwwroot/js/global.min.js ${{ env.outputFolder }}/wwwroot/js/global.min.js + mkdir -p ${{ env.outputFolder }}/wwwroot/font + rsync -ar ${{ github.workspace }}/WebfrontCore/wwwroot/lib/open-iconic/font/fonts/ ${{ env.outputFolder }}/wwwroot/font + mkdir -p ${{ env.outputFolder }}/GameFiles + rsync -ar ${{ github.workspace }}/GameFiles/ ${{ env.outputFolder }}/GameFiles + mkdir -p ${{ env.outputFolder }}/wwwroot/images/ + rsync -ar ${{ github.workspace }}/WebfrontCore/wwwroot/images/ ${{ env.outputFolder }}/wwwroot/images/ + rsync -ar ${{ github.workspace }}/BUILD/Plugins/wwwroot/ ${{ env.outputFolder }}/wwwroot/ + + - name: Upload artifact for analysis + uses: actions/upload-artifact@v4 + with: + name: IW4MAdmin-${{ env.buildNumber }}-${{ env.releaseType }} + path: ${{ env.outputFolder }} + + release_github: + runs-on: ubuntu-latest + needs: [ make_version, build ] + permissions: + contents: write + environment: prerelease + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/release/pre' }} + + env: + buildNumber: ${{ needs.make_version.outputs.build_num }} + + steps: + - name: Download build + uses: actions/download-artifact@v4 + with: + name: IW4MAdmin-${{ env.buildNumber }}-${{ env.releaseType }} + path: ${{ github.workspace }} + + - name: Zip build + run: zip -r IW4MAdmin-${{ env.buildNumber }}.zip ${{ github.workspace }}/* + + - name: Make release + uses: ncipollo/release-action@v1 + with: + tag: ${{ env.buildNumber }}-${{ env.releaseType }} + name: IW4MAdmin ${{ env.buildNumber }} + draft: false + prerelease: true + body: Automated rolling release - changelog below. [Updating Instructions](https://github.com/RaidMax/IW4M-Admin/wiki/Getting-Started#updating) + generateReleaseNotes: true + artifacts: ${{ github.workspace }}/*.zip + artifactErrorsFailBuild: true + + update_master_version: + runs-on: ubuntu-latest + needs: [ make_version, build, release_github ] + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/release/pre' }} + + env: + buildNumber: ${{ needs.make_version.outputs.build_num }} + + steps: + - name: Update master version + run: | + curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"current-version-${{ env.releaseType }}":"${{ env.buildNumber }}","jwt-secret": "${{ secrets.JWTSecret }}"}' \ + http://api.raidmax.org:5000/version diff --git a/.github/workflows/shared_library_nuget.yml b/.github/workflows/shared_library_nuget.yml new file mode 100644 index 00000000..109c0d10 --- /dev/null +++ b/.github/workflows/shared_library_nuget.yml @@ -0,0 +1,93 @@ +name: SharedLibraryCore NuGet + +on: + push: + branches: [ develop, release/pre, master ] + paths: + - SharedLibraryCore/** + - Data/** + - .github/workflows/shared_library_nuget.yml + pull_request: + branches: [ develop ] + paths: + - SharedLibraryCore/** + - Data/** + +env: + outputDirectory: ${{ github.workspace}}/nuget + +jobs: + make_version: + runs-on: ubuntu-latest + outputs: + build_num: ${{ steps.generate_build_number.outputs.build_num }} + + steps: + - name: Make build number + id: generate_build_number + run: | + run_number=$(git log --since=$(date +'%Y-%m-%dT00:00:00') --oneline | grep -c 'workflow_run') + run_number=$((run_number + 1)) + build_num=$(date +'%Y.%-m.%-d').$(run_number) + echo "build_num=$build_num" >> $GITHUB_OUTPUT + echo "Build number is $build_num" + + build_pack: + runs-on: ubuntu-latest + needs: [ make_version ] + + env: + buildNumber: ${{ needs.make_version.outputs.build_num }} + packageTag: ${{ github.event_name == 'pull_request' && '-beta' || '-preview' }} + buildConfiguration: Prerelease + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build data + run: dotnet build **/Data.csproj -c ${{env.buildConfiguration}} /p:Version=${{ env.buildNumber }} --no-restore + + - name: Build SLC + run: dotnet build **/SharedLibraryCore.csproj -c ${{env.buildConfiguration}} /p:Version=${{ env.buildNumber }} --no-restore + + - name: Pack SLC + run: dotnet pack **/SharedLibraryCore.csproj -c ${{env.buildConfiguration}} -p:PackageVersion=${{ env.buildNumber }}${{ env.packageTag }} -o ${{ env.outputDirectory }} --no-restore + + - name: Publish nuget package artifact + uses: actions/upload-artifact@v4 + with: + name: SharedLibraryCore-${{ env.buildNumber }} + path: ${{ env.outputDirectory }}/*.nupkg + + publish: + runs-on: ubuntu-latest + + needs: [ make_version, build_pack ] + environment: prerelease + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/release/pre' || github.ref == 'refs/heads/develop' }} + + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: SharedLibraryCore-${{ needs.make_version.outputs.build_num }} + path: ${{ env.outputDirectory }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Publish NuGet package + run: | + for file in ${{ env.outputDirectory }}/*.nupkg; do + dotnet nuget push "$file" --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate + done diff --git a/.gitignore b/.gitignore index 62c99e06..3c177de0 100644 --- a/.gitignore +++ b/.gitignore @@ -247,3 +247,4 @@ launchSettings.json *.db /Data/IW4MAdmin_Migration.db-shm /Data/IW4MAdmin_Migration.db-wal +bundle/ \ No newline at end of file diff --git a/Application/API/GameLogServer/IGameLogServer.cs b/Application/API/GameLogServer/IGameLogServer.cs index 302c9040..a0eb960b 100644 --- a/Application/API/GameLogServer/IGameLogServer.cs +++ b/Application/API/GameLogServer/IGameLogServer.cs @@ -1,12 +1,12 @@ using System.Threading.Tasks; -using RestEase; +using Refit; namespace IW4MAdmin.Application.API.GameLogServer { - [Header("User-Agent", "IW4MAdmin-RestEase")] + [Headers("User-Agent: IW4MAdmin-RestEase")] public interface IGameLogServer { - [Get("log/{path}/{key}")] - Task Log([Path] string path, [Path] string key); + [Get("/log/{path}/{key}")] + Task Log(string path, string key); } } diff --git a/Application/API/GameLogServer/LogInfo.cs b/Application/API/GameLogServer/LogInfo.cs index 612d4cdc..0286d096 100644 --- a/Application/API/GameLogServer/LogInfo.cs +++ b/Application/API/GameLogServer/LogInfo.cs @@ -1,19 +1,16 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; +using System.Text.Json.Serialization; namespace IW4MAdmin.Application.API.GameLogServer { public class LogInfo { - [JsonProperty("success")] + [JsonPropertyName("success")] public bool Success { get; set; } - [JsonProperty("length")] + [JsonPropertyName("length")] public int Length { get; set; } - [JsonProperty("data")] + [JsonPropertyName("data")] public string Data { get; set; } - [JsonProperty("next_key")] + [JsonPropertyName("next_key")] public string NextKey { get; set; } } } diff --git a/Application/API/Master/ApiInstance.cs b/Application/API/Master/ApiInstance.cs index 01a98baf..c70fdca4 100644 --- a/Application/API/Master/ApiInstance.cs +++ b/Application/API/Master/ApiInstance.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using SharedLibraryCore.Helpers; namespace IW4MAdmin.Application.API.Master @@ -12,32 +12,32 @@ namespace IW4MAdmin.Application.API.Master /// /// Unique ID of the instance /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; set; } /// /// Indicates how long the instance has been running /// - [JsonProperty("uptime")] + [JsonPropertyName("uptime")] public int Uptime { get; set; } /// /// Specifies the version of the instance /// - [JsonProperty("version")] + [JsonPropertyName("version")] [JsonConverter(typeof(BuildNumberJsonConverter))] public BuildNumber Version { get; set; } /// /// List of servers the instance is monitoring /// - [JsonProperty("servers")] + [JsonPropertyName("servers")] public List Servers { get; set; } /// /// Url IW4MAdmin is listening on /// - [JsonProperty("webfront_url")] + [JsonPropertyName("webfront_url")] public string WebfrontUrl { get; set; } } } diff --git a/Application/API/Master/ApiServer.cs b/Application/API/Master/ApiServer.cs index 123a5852..4c2fe3ca 100644 --- a/Application/API/Master/ApiServer.cs +++ b/Application/API/Master/ApiServer.cs @@ -1,31 +1,28 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; +using System.Text.Json.Serialization; namespace IW4MAdmin.Application.API.Master { public class ApiServer { - [JsonProperty("id")] + [JsonPropertyName("id")] public long Id { get; set; } - [JsonProperty("ip")] + [JsonPropertyName("ip")] public string IPAddress { get; set; } - [JsonProperty("port")] + [JsonPropertyName("port")] public short Port { get; set; } - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } - [JsonProperty("gametype")] + [JsonPropertyName("gametype")] public string Gametype { get; set; } - [JsonProperty("map")] + [JsonPropertyName("map")] public string Map { get; set; } - [JsonProperty("game")] + [JsonPropertyName("game")] public string Game { get; set; } - [JsonProperty("hostname")] + [JsonPropertyName("hostname")] public string Hostname { get; set; } - [JsonProperty("clientnum")] + [JsonPropertyName("clientnum")] public int ClientNum { get; set; } - [JsonProperty("maxclientnum")] + [JsonPropertyName("maxclientnum")] public int MaxClientNum { get; set; } } } diff --git a/Application/API/Master/IMasterApi.cs b/Application/API/Master/IMasterApi.cs index c96689f7..b32d73f1 100644 --- a/Application/API/Master/IMasterApi.cs +++ b/Application/API/Master/IMasterApi.cs @@ -1,79 +1,70 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using System.Threading.Tasks; using IW4MAdmin.Application.Plugin; -using Newtonsoft.Json; -using RestEase; +using Refit; using SharedLibraryCore.Helpers; -namespace IW4MAdmin.Application.API.Master +namespace IW4MAdmin.Application.API.Master; + +public class AuthenticationId { - public class AuthenticationId - { - [JsonProperty("id")] - public string Id { get; set; } - } - - public class TokenId - { - [JsonProperty("access_token")] - public string AccessToken { get; set; } - } - - public class VersionInfo - { - [JsonProperty("current-version-stable")] - [JsonConverter(typeof(BuildNumberJsonConverter))] - public BuildNumber CurrentVersionStable { get; set; } - - [JsonProperty("current-version-prerelease")] - [JsonConverter(typeof(BuildNumberJsonConverter))] - public BuildNumber CurrentVersionPrerelease { get; set; } - } - - public class ResultMessage - { - [JsonProperty("message")] - public string Message { get; set; } - } - - public class PluginSubscriptionContent - { - public string Content { get; set; } - public PluginType Type { get; set; } - } - - - /// - /// Defines the capabilities of the master API - /// - [Header("User-Agent", "IW4MAdmin-RestEase")] - public interface IMasterApi - { - [Header("Authorization")] - string AuthorizationToken { get; set; } - - [Post("authenticate")] - Task Authenticate([Body] AuthenticationId Id); - - [Post("instance/")] - [AllowAnyStatusCode] - Task> AddInstance([Body] ApiInstance instance); - - [Put("instance/{id}")] - [AllowAnyStatusCode] - Task> UpdateInstance([Path] string id, [Body] ApiInstance instance); - - [Get("version/{apiVersion}")] - Task GetVersion([Path] int apiVersion); - - [Get("localization")] - Task> GetLocalization(); - - [Get("localization/{languageTag}")] - Task GetLocalization([Path("languageTag")] string languageTag); - - [Get("plugin_subscriptions")] - Task> GetPluginSubscription([Query("instance_id")] Guid instanceId, [Query("subscription_id")] string subscription_id); - } + [JsonPropertyName("id")] public string Id { get; set; } +} + +public class TokenId +{ + [JsonPropertyName("access_token")] public string AccessToken { get; set; } +} + +public class VersionInfo +{ + [JsonPropertyName("current-version-stable")] + [JsonConverter(typeof(BuildNumberJsonConverter))] + public BuildNumber CurrentVersionStable { get; set; } + + [JsonPropertyName("current-version-prerelease")] + [JsonConverter(typeof(BuildNumberJsonConverter))] + public BuildNumber CurrentVersionPrerelease { get; set; } +} + +public class ResultMessage +{ + [JsonPropertyName("message")] public string Message { get; set; } +} + +public class PluginSubscriptionContent +{ + public string Content { get; set; } + public PluginType Type { get; set; } +} + +/// +/// Defines the capabilities of the master API +/// +[Headers("User-Agent: IW4MAdmin-RestEase")] +public interface IMasterApi +{ + [Post("/authenticate")] + Task Authenticate([Body] AuthenticationId Id); + + [Post("/instance/")] + Task> AddInstance([Body] ApiInstance instance, [Header("Authorization")] string authorization); + + [Put("/instance/{id}")] + Task> UpdateInstance(string id, [Body] ApiInstance instance, [Header("Authorization")] string authorization); + + [Get("/version/{apiVersion}")] + Task GetVersion(int apiVersion); + + [Get("/localization")] + Task> GetLocalization(); + + [Get("/localization/{languageTag}")] + Task GetLocalization(string languageTag); + + [Get("/plugin_subscriptions")] + Task> GetPluginSubscription([Query] string instance_id, + [Query] string subscription_id); } diff --git a/Application/Application.csproj b/Application/Application.csproj index 858f10a5..cfade8ce 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 false RaidMax.IW4MAdmin.Application 2020.0.0.0 @@ -21,20 +21,21 @@ IW4MAdmin.Application false + disable - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + @@ -72,18 +73,8 @@ - - - - - - - - - - - - - + + + diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 98e39512..718b8db9 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -26,6 +26,7 @@ using Data.Abstractions; using Data.Context; using Data.Models; using IW4MAdmin.Application.Configuration; +using IW4MAdmin.Application.IO; using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Plugin.Script; using Microsoft.Extensions.DependencyInjection; @@ -68,6 +69,7 @@ namespace IW4MAdmin.Application private readonly ClientService ClientSvc; readonly PenaltyService PenaltySvc; private readonly IAlertManager _alertManager; + private readonly ConfigurationWatcher _watcher; public IConfigurationHandler ConfigHandler; readonly IPageList PageList; private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); @@ -94,7 +96,8 @@ namespace IW4MAdmin.Application IEnumerable plugins, IParserRegexFactory parserRegexFactory, IEnumerable 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 v2PLugins) + ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable v2PLugins, + ConfigurationWatcher watcher) { MiddlewareActionHandler = actionHandler; _servers = new ConcurrentBag(); @@ -102,10 +105,11 @@ namespace IW4MAdmin.Application ClientSvc = clientService; PenaltySvc = penaltyService; _alertManager = alertManager; + _watcher = watcher; ConfigHandler = appConfigHandler; StartTime = DateTime.UtcNow; PageList = new PageList(); - AdditionalEventParsers = new List { new BaseEventParser(parserRegexFactory, logger, _appConfig) }; + AdditionalEventParsers = new List { new BaseEventParser(parserRegexFactory, logger, _appConfig, serviceProvider.GetRequiredService()) }; AdditionalRConParsers = new List { new BaseRConParser(serviceProvider.GetRequiredService>(), parserRegexFactory) }; TokenAuthenticator = new TokenAuthentication(); _logger = logger; @@ -529,6 +533,7 @@ namespace IW4MAdmin.Application Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]); await InitializeServers(); + _watcher.Enable(); IsInitialized = true; } @@ -710,7 +715,7 @@ namespace IW4MAdmin.Application public IEventParser GenerateDynamicEventParser(string name) { - return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration()) + return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration(), _serviceProvider.GetRequiredService()) { Name = name }; diff --git a/Application/BuildScripts/DownloadTranslations.ps1 b/Application/BuildScripts/DownloadTranslations.ps1 deleted file mode 100644 index 7286ca4b..00000000 --- a/Application/BuildScripts/DownloadTranslations.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -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 -} diff --git a/Application/BuildScripts/PostBuild.bat b/Application/BuildScripts/PostBuild.bat deleted file mode 100644 index c57f8264..00000000 --- a/Application/BuildScripts/PostBuild.bat +++ /dev/null @@ -1,17 +0,0 @@ -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%SharedLibraryCore\%OutDir%*.deps.json" "%TargetDir%" - -if not exist "%TargetDir%Plugins" ( - echo "Making plugin dir" - md "%TargetDir%Plugins" -) - -xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\" -del "%TargetDir%Plugins\SQLite*" diff --git a/Application/BuildScripts/PostBuild.sh b/Application/BuildScripts/PostBuild.sh new file mode 100644 index 00000000..f855be56 --- /dev/null +++ b/Application/BuildScripts/PostBuild.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +PublishDir="$1" +SourceDir="$2" + +if [ -z "$PublishDir" ] || [ -z "$SourceDir" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Deleting extra runtime files" +declare -a runtimes=("linux-arm" "linux-arm64" "linux-armel" "osx" "osx-x64" "win-arm" "win-arm64" "alpine-x64" "linux-musl-x64") +for runtime in "${runtimes[@]}"; do + if [ -d "$PublishDir/runtimes/$runtime" ]; then + rm -rf "$PublishDir/runtimes/$runtime" + fi +done + +echo "Deleting misc files" +if [ -f "$PublishDir/web.config" ]; then rm "$PublishDir/web.config"; fi +if [ -f "$PublishDir/libman.json" ]; then rm "$PublishDir/libman.json"; fi +rm -f "$PublishDir"/*.exe +rm -f "$PublishDir"/*.pdb +rm -f "$PublishDir"/IW4MAdmin + +echo "Setting up default folders" +mkdir -p "$PublishDir/Plugins" +mkdir -p "$PublishDir/Configuration" +mv "$PublishDir/DefaultSettings.json" "$PublishDir/Configuration/" + +mkdir -p "$PublishDir/Lib" +rm -f "$PublishDir/Microsoft.CodeAnalysis*.dll" +mv "$PublishDir"/*.dll "$PublishDir/Lib/" +mv "$PublishDir"/*.json "$PublishDir/Lib/" +mv "$PublishDir/runtimes" "$PublishDir/Lib/runtimes" +mv "$PublishDir/ru" "$PublishDir/Lib/ru" +mv "$PublishDir/de" "$PublishDir/Lib/de" +mv "$PublishDir/pt" "$PublishDir/Lib/pt" +mv "$PublishDir/es" "$PublishDir/Lib/es" +rm -rf "$PublishDir/cs" +rm -rf "$PublishDir/fr" +rm -rf "$PublishDir/it" +rm -rf "$PublishDir/ja" +rm -rf "$PublishDir/ko" +rm -rf "$PublishDir/pl" +rm -rf "$PublishDir/pt-BR" +rm -rf "$PublishDir/tr" +rm -rf "$PublishDir/zh-Hans" +rm -rf "$PublishDir/zh-Hant" +if [ -d "$PublishDir/refs" ]; then mv "$PublishDir/refs" "$PublishDir/Lib/refs"; fi diff --git a/Application/BuildScripts/PostPublish.bat b/Application/BuildScripts/PostPublish.bat deleted file mode 100644 index 53c22068..00000000 --- a/Application/BuildScripts/PostPublish.bat +++ /dev/null @@ -1,67 +0,0 @@ -set PublishDir=%1 -set SourceDir=%2 -SET COPYCMD=/Y - -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' - -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" - -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 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" - -echo copying update scripts -copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.ps1" "%PublishDir%\UpdateIW4MAdmin.ps1" -copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.sh" "%PublishDir%\UpdateIW4MAdmin.sh" - -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 - - -echo setting permissions... -cacls "%PublishDir%" /t /e /p Everyone:F diff --git a/Application/BuildScripts/PreBuild.bat b/Application/BuildScripts/PreBuild.bat deleted file mode 100644 index 876350c2..00000000 --- a/Application/BuildScripts/PreBuild.bat +++ /dev/null @@ -1,6 +0,0 @@ -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% \ No newline at end of file diff --git a/Application/BuildScripts/PreBuild.ps1 b/Application/BuildScripts/PreBuild.ps1 new file mode 100644 index 00000000..300f97e5 --- /dev/null +++ b/Application/BuildScripts/PreBuild.ps1 @@ -0,0 +1,59 @@ +param ( [string]$SolutionDir, [string]$OutputDir ) + +if (-not (Test-Path "$SolutionDir/WebfrontCore/wwwroot/font")) { + Write-Output "restoring web dependencies" + dotnet tool install Microsoft.Web.LibraryManager.Cli --global + Set-Location "$SolutionDir/WebfrontCore" + libman restore + Set-Location $SolutionDir + Copy-Item -Recurse -Force -Path "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/fonts" "$SolutionDir/WebfrontCore/wwwroot/font" +} + +if (-not (Test-Path "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss")) { + Write-Output "load external resources" + New-Item -ItemType Directory -Force -Path "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css" + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss" -OutFile "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" + (Get-Content "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss") -replace '../fonts/', '/font/' | Set-Content "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" +} + +Write-Output "compiling scss files" + +dotnet tool install Excubo.WebCompiler --global +webcompiler -r "$SolutionDir/WebfrontCore/wwwroot/css/src" -o WebfrontCore/wwwroot/css/ -m disable -z disable +webcompiler "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" -o "$SolutionDir/WebfrontCore/wwwroot/css/" -m disable -z disable + +if (-not (Test-Path "$SolutionDir/bundle/dotnet-bundle.dll")) { + New-Item -ItemType Directory -Force -Path "$SolutionDir/bundle" + Write-Output "getting dotnet bundle" + Invoke-WebRequest -Uri "https://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip" -OutFile "$SolutionDir/bundle/dotnet-bundle.zip" + Write-Output "unzipping download" + Expand-Archive -Path "$SolutionDir/bundle/dotnet-bundle.zip" -DestinationPath "$SolutionDir/bundle" -Force +} + +Write-Output "executing dotnet-bundle" +Set-Location "$SolutionDir/bundle" +dotnet "dotnet-bundle.dll" clean "$SolutionDir/WebfrontCore/bundleconfig.json" +dotnet "dotnet-bundle.dll" "$SolutionDir/WebfrontCore/bundleconfig.json" +Set-Location $SolutionDir + +New-Item -ItemType Directory -Force -Path "$SolutionDir/BUILD/Plugins" +Write-Output "building plugins" +Set-Location "$SolutionDir/Plugins" +Get-ChildItem -Recurse -Filter *.csproj | ForEach-Object { dotnet publish $_.FullName -o "$SolutionDir/BUILD/Plugins" --no-restore } +Set-Location $SolutionDir + +if (-not (Test-Path "$OutputDir/Localization")) { + Write-Output "downloading translations" + New-Item -ItemType Directory -Force -Path "$OutputDir/Localization" + $localizations = @("en-US", "ru-RU", "es-EC", "pt-BR", "de-DE") + foreach ($localization in $localizations) { + $url = "https://master.iw4.zip/localization/$localization" + $filePath = "$OutputDir/Localization/IW4MAdmin.$localization.json" + Invoke-WebRequest -Uri $url -OutFile $filePath -UseBasicParsing + } +} + +Write-Output "copying plugins to build dir" +New-Item -ItemType Directory -Force -Path "$OutputDir/Plugins" +Copy-Item -Recurse -Force -Path "$SolutionDir/BUILD/Plugins/*.dll" -Destination "$OutputDir/Plugins/" +Copy-Item -Recurse -Force -Path "$SolutionDir/Plugins/ScriptPlugins/*.js" -Destination "$OutputDir/Plugins/" diff --git a/Application/BuildScripts/PreBuild.sh b/Application/BuildScripts/PreBuild.sh new file mode 100644 index 00000000..18765093 --- /dev/null +++ b/Application/BuildScripts/PreBuild.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +SolutionDir="$1" +OutputDir="$2" +export PATH="$PATH:~/.dotnet/tools" + +if [ ! -d "$SolutionDir/WebfrontCore/wwwroot/lib/font" ]; then + echo restoring web dependencies + dotnet tool install Microsoft.Web.LibraryManager.Cli --global + cd "$SolutionDir/WebfrontCore" || exit + libman restore + cd "$SolutionDir" || exit + cp -r "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/fonts" "$SolutionDir/WebfrontCore/wwwroot/font" +fi + +if [ ! -f "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" ]; then + echo load external resources + mkdir -p "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css" + curl -o "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss + sed -i 's#../fonts/#/font/#g' "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" +fi + +echo compiling scss files + +dotnet tool install Excubo.WebCompiler --global +webcompiler -r "$SolutionDir/WebfrontCore/wwwroot/css/src" -o "$SolutionDir/WebfrontCore/wwwroot/css/" -m disable -z disable +webcompiler "$SolutionDir/WebfrontCore/wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" -o "$SolutionDir/WebfrontCore/wwwroot/css/" -m disable -z disable + +if [ ! -f "$SolutionDir/bundle/dotnet-bundle.dll" ]; then + mkdir -p "$SolutionDir/bundle" + echo getting dotnet bundle + curl -o "$SolutionDir/bundle/dotnet-bundle.zip" https://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip + echo unzipping download + unzip "$SolutionDir/bundle/dotnet-bundle.zip" -d "$SolutionDir/bundle" +fi +echo executing dotnet-bundle +cd "$SolutionDir/bundle" || exit +dotnet dotnet-bundle.dll clean "$SolutionDir/WebfrontCore/bundleconfig.json" +dotnet dotnet-bundle.dll "$SolutionDir/WebfrontCore/bundleconfig.json" +cd "$SolutionDir" || exit + +mkdir -p "$SolutionDir/BUILD/Plugins" +echo building plugins +cd "$SolutionDir/Plugins" || exit +find . -name "*.csproj" -print0 | xargs -0 -I {} dotnet publish {} -o "$SolutionDir/BUILD/Plugins" --no-restore +cd "$SolutionDir" || exit + +if [ ! -d "$OutputDir/Localization" ]; then + echo downloading translations + mkdir -p "$OutputDir/Localization" + localizations=("en-US" "ru-RU" "es-EC" "pt-BR" "de-DE") + for localization in "${localizations[@]}" + do + url="https://master.iw4.zip/localization/$localization" + filePath="$OutputDir/Localization/IW4MAdmin.$localization.json" + curl -s "$url" -o "$filePath" + done +fi + +echo copying plugins to buld dir +mkdir -p "$OutputDir/Plugins" +cp -r "$SolutionDir/BUILD/Plugins/*.dll" "$OutputDir/Plugins/" +cp -r "$SolutionDir/Plugins/ScriptPlugins/*.js" "$OutputDir/Plugins/" diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs index 3f49be57..105e2ec7 100644 --- a/Application/EventParsers/BaseEventParser.cs +++ b/Application/EventParsers/BaseEventParser.cs @@ -8,6 +8,7 @@ using System.Linq; using Data.Models; using Microsoft.Extensions.Logging; using SharedLibraryCore.Events.Game; +using SharedLibraryCore.Interfaces.Events; using static System.Int32; using static SharedLibraryCore.Server; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -21,16 +22,18 @@ namespace IW4MAdmin.Application.EventParsers private readonly ILogger _logger; private readonly ApplicationConfiguration _appConfig; + private readonly IGameScriptEventFactory _gameScriptEventFactory; private readonly Dictionary _regexMap; private readonly Dictionary _eventTypeMap; public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, - ApplicationConfiguration appConfig) + ApplicationConfiguration appConfig, IGameScriptEventFactory gameScriptEventFactory) { _customEventRegistrations = new Dictionary)>(); _logger = logger; _appConfig = appConfig; + _gameScriptEventFactory = gameScriptEventFactory; Configuration = new DynamicEventParserConfiguration(parserRegexFactory) { @@ -183,14 +186,28 @@ namespace IW4MAdmin.Application.EventParsers if (logLine.StartsWith("GSE;")) { - return new GameScriptEvent + var gscEvent = new GameScriptEvent { ScriptData = logLine, GameTime = gameTime, Source = GameEvent.EventSource.Log }; + return gscEvent; } + var split = logLine.Split(";", StringSplitOptions.RemoveEmptyEntries); + + if (split.Length > 1) + { + var createdEvent = _gameScriptEventFactory.Create(split[0], logLine.Replace(split[0], "")); + if (createdEvent is not null) + { + createdEvent.ParseArguments(); + return createdEvent as GameEventV2; + } + } + + if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey)) { return GenerateDefaultEvent(logLine, gameTime); @@ -218,12 +235,13 @@ namespace IW4MAdmin.Application.EventParsers return GenerateDefaultEvent(logLine, gameTime); } - private static GameEvent GenerateDefaultEvent(string logLine, long gameTime) + private static GameLogEvent GenerateDefaultEvent(string logLine, long gameTime) { - return new GameEvent + return new GameLogEvent { Type = GameEvent.EventType.Unknown, Data = logLine, + LogLine = logLine, Origin = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(), RequiredEntity = GameEvent.EventRequiredEntity.None, diff --git a/Application/EventParsers/DynamicEventParser.cs b/Application/EventParsers/DynamicEventParser.cs index 8c8c0fb3..2a6378c9 100644 --- a/Application/EventParsers/DynamicEventParser.cs +++ b/Application/EventParsers/DynamicEventParser.cs @@ -1,5 +1,6 @@ using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; +using SharedLibraryCore.Interfaces.Events; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.EventParsers @@ -10,7 +11,9 @@ namespace IW4MAdmin.Application.EventParsers /// sealed internal class DynamicEventParser : BaseEventParser { - public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) : base(parserRegexFactory, logger, appConfig) + public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, + ApplicationConfiguration appConfig, IGameScriptEventFactory gameScriptEventFactory) : base( + parserRegexFactory, logger, appConfig, gameScriptEventFactory) { } } diff --git a/Application/Extensions/ScriptPluginExtensions.cs b/Application/Extensions/ScriptPluginExtensions.cs index b6a2c87c..bc6fd5e7 100644 --- a/Application/Extensions/ScriptPluginExtensions.cs +++ b/Application/Extensions/ScriptPluginExtensions.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; +using Data.Models.Client; using Data.Models.Client.Stats; using Microsoft.EntityFrameworkCore; +using SharedLibraryCore.Interfaces; namespace IW4MAdmin.Application.Extensions; @@ -25,4 +27,16 @@ public static class ScriptPluginExtensions { return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList(); } + + public static EFClient GetClientByNumber(this IGameServer server, int clientNumber) => + server.ConnectedClients.FirstOrDefault(client => client.ClientNumber == clientNumber); + + public static EFClient GetClientByGuid(this IGameServer server, string clientGuid) => + server.ConnectedClients.FirstOrDefault(client => client?.GuidString == clientGuid?.Trim().ToLower()); + + public static EFClient GetClientByXuid(this IGameServer server, string clientGuid) => + server.ConnectedClients.FirstOrDefault(client => client?.XuidString == clientGuid?.Trim().ToLower()); + + public static EFClient GetClientByDecimalGuid(this IGameServer server, string clientGuid) => + server.ConnectedClients.FirstOrDefault(client => client.NetworkId.ToString() == clientGuid?.Trim().ToLower()); } diff --git a/Application/Factories/GameScriptEventFactory.cs b/Application/Factories/GameScriptEventFactory.cs new file mode 100644 index 00000000..3ffb7985 --- /dev/null +++ b/Application/Factories/GameScriptEventFactory.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using SharedLibraryCore.Interfaces.Events; + +namespace IW4MAdmin.Application.Factories; + +public class GameScriptEventFactory : IGameScriptEventFactory +{ + private readonly IServiceProvider _serviceProvider; + private readonly Dictionary _gameScriptEventMap; + + public GameScriptEventFactory(IServiceProvider serviceProvider, IEnumerable gameScriptEventTypes) + { + _serviceProvider = serviceProvider; + + _gameScriptEventMap = gameScriptEventTypes + .ToLookup(kvp => kvp.EventName ?? kvp.GetType().Name.Replace("ScriptEvent", "")) + .ToDictionary(kvp => kvp.Key, kvp => kvp.First().GetType()); + } + + public IGameScriptEvent Create(string eventType, string logData) + { + if (string.IsNullOrEmpty(eventType) || !_gameScriptEventMap.TryGetValue(eventType, out var matchedType)) + { + return null; + } + + var newEvent = _serviceProvider.GetRequiredService(matchedType) as IGameScriptEvent; + + if (newEvent is not null) + { + newEvent.ScriptData = logData; + } + + return newEvent; + } +} diff --git a/Application/IO/BaseConfigurationHandlerV2.cs b/Application/IO/BaseConfigurationHandlerV2.cs index fee9d14f..07e427a9 100644 --- a/Application/IO/BaseConfigurationHandlerV2.cs +++ b/Application/IO/BaseConfigurationHandlerV2.cs @@ -71,7 +71,6 @@ public class BaseConfigurationHandlerV2 : IConfigurationHand await using var fileStream = File.OpenRead(_path); readConfiguration = await JsonSerializer.DeserializeAsync(fileStream, _serializerOptions); - await fileStream.DisposeAsync(); _watcher.Register(_path, FileUpdated); if (readConfiguration is null) @@ -131,7 +130,6 @@ public class BaseConfigurationHandlerV2 : IConfigurationHand await using var fileStream = File.Create(_path); await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions); - await fileStream.DisposeAsync(); _configurationInstance = configuration; } catch (Exception ex) @@ -155,7 +153,6 @@ public class BaseConfigurationHandlerV2 : IConfigurationHand await using var fileStream = File.OpenRead(_path); var readConfiguration = await JsonSerializer.DeserializeAsync(fileStream, _serializerOptions); - await fileStream.DisposeAsync(); if (readConfiguration is null) { diff --git a/Application/IO/ConfigurationWatcher.cs b/Application/IO/ConfigurationWatcher.cs index 979b7e60..8df130de 100644 --- a/Application/IO/ConfigurationWatcher.cs +++ b/Application/IO/ConfigurationWatcher.cs @@ -20,7 +20,6 @@ public sealed class ConfigurationWatcher : IDisposable }; _watcher.Changed += WatcherOnChanged; - _watcher.EnableRaisingEvents = true; } public void Dispose() @@ -31,30 +30,28 @@ public sealed class ConfigurationWatcher : IDisposable public void Register(string fileName, Action fileUpdated) { - if (_registeredActions.ContainsKey(fileName)) - { - return; - } - - _registeredActions.Add(fileName, fileUpdated); + _registeredActions.TryAdd(fileName, fileUpdated); } public void Unregister(string fileName) { - if (_registeredActions.ContainsKey(fileName)) - { - _registeredActions.Remove(fileName); - } + _registeredActions.Remove(fileName); + } + + public void Enable() + { + _watcher.EnableRaisingEvents = true; } private void WatcherOnChanged(object sender, FileSystemEventArgs eventArgs) { - if (!_registeredActions.ContainsKey(eventArgs.FullPath) || eventArgs.ChangeType != WatcherChangeTypes.Changed || + if (!_registeredActions.TryGetValue(eventArgs.FullPath, out var value) || + eventArgs.ChangeType != WatcherChangeTypes.Changed || new FileInfo(eventArgs.FullPath).Length == 0) { return; } - _registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath); + value.Invoke(eventArgs.FullPath); } } diff --git a/Application/IO/GameLogEventDetection.cs b/Application/IO/GameLogEventDetection.cs index eca6fb8c..54b8b9f6 100644 --- a/Application/IO/GameLogEventDetection.cs +++ b/Application/IO/GameLogEventDetection.cs @@ -82,7 +82,7 @@ namespace IW4MAdmin.Application.IO { 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);; + gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId); } if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target) diff --git a/Application/IO/GameLogReaderHttp.cs b/Application/IO/GameLogReaderHttp.cs index a585fa89..16a4437d 100644 --- a/Application/IO/GameLogReaderHttp.cs +++ b/Application/IO/GameLogReaderHttp.cs @@ -1,5 +1,4 @@ using IW4MAdmin.Application.API.GameLogServer; -using RestEase; using SharedLibraryCore; using SharedLibraryCore.Interfaces; using System; @@ -7,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Refit; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.IO @@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.IO public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger) { _eventParser = parser; - _logServerApi = RestClient.For(gameLogServerUris[0].ToString()); + _logServerApi = RestService.For(gameLogServerUris[0].ToString()); _safeLogPath = gameLogServerUris[1].LocalPath.ToBase64UrlSafeString(); _logger = logger; } diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index bbde0c41..129f239c 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -25,6 +25,7 @@ using Serilog.Context; using static SharedLibraryCore.Database.Models.EFClient; using Data.Models; using Data.Models.Server; +using Humanizer; using IW4MAdmin.Application.Alerts; using IW4MAdmin.Application.Commands; using IW4MAdmin.Application.Plugin.Script; @@ -193,18 +194,54 @@ namespace IW4MAdmin Command command = null; if (E.Type == GameEvent.EventType.Command) { + if (E.Origin is not null) + { + var canExecute = true; + + if (E.Origin.CommandExecutionAttempts > 0 && E.Origin.Level < Permission.Trusted) + { + var remainingTimeout = + E.Origin.LastCommandExecutionAttempt + + Utilities.GetExponentialBackoffDelay(E.Origin.CommandExecutionAttempts) - + DateTimeOffset.UtcNow; + + if (remainingTimeout.TotalSeconds > 0) + { + if (E.Origin.CommandExecutionAttempts < 2 || + E.Origin.CommandExecutionAttempts % 5 == 0) + { + E.Origin.Tell(_translationLookup["COMMANDS_BACKOFF_MESSAGE"] + .FormatExt(remainingTimeout.Humanize())); + } + + canExecute = false; + } + else + { + E.Origin.CommandExecutionAttempts = 0; + } + } + + E.Origin.LastCommandExecutionAttempt = DateTimeOffset.UtcNow; + E.Origin.CommandExecutionAttempts++; + + if (!canExecute) + { + return; + } + } + try { command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration); } - catch (CommandException e) { ServerLogger.LogWarning(e, "Error validating command from event {@Event}", new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId }); E.FailReason = GameEvent.EventFailReason.Invalid; } - + if (command != null) { E.Extra = command; diff --git a/Application/Localization/Configure.cs b/Application/Localization/Configure.cs index b2155e97..abe5b731 100644 --- a/Application/Localization/Configure.cs +++ b/Application/Localization/Configure.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; +using System.Text.Json; using Microsoft.Extensions.Logging; using SharedLibraryCore.Configuration; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -59,11 +60,11 @@ namespace IW4MAdmin.Application.Localization var localizationDict = new Dictionary(); - foreach (string filePath in localizationFiles) + foreach (var filePath in localizationFiles) { var localizationContents = File.ReadAllText(filePath, Encoding.UTF8); - var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject(localizationContents); - if (eachLocalizationFile == null) + var eachLocalizationFile = JsonSerializer.Deserialize(localizationContents); + if (eachLocalizationFile is null) { continue; } @@ -72,7 +73,7 @@ namespace IW4MAdmin.Application.Localization { if (!localizationDict.TryAdd(item.Key, item.Value)) { - logger.LogError("Could not add locale string {key} to localization", item.Key); + logger.LogError("Could not add locale string {Key} to localization", item.Key); } } } diff --git a/Application/Main.cs b/Application/Main.cs index bcded032..0362688b 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -5,7 +5,6 @@ using IW4MAdmin.Application.Meta; using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Misc; using Microsoft.Extensions.DependencyInjection; -using RestEase; using SharedLibraryCore; using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models; @@ -39,6 +38,8 @@ using ILogger = Microsoft.Extensions.Logging.ILogger; using IW4MAdmin.Plugins.Stats.Client.Abstractions; using IW4MAdmin.Plugins.Stats.Client; using Microsoft.Extensions.Hosting; +using Refit; +using SharedLibraryCore.Interfaces.Events; using Stats.Client.Abstractions; using Stats.Client; using Stats.Config; @@ -94,15 +95,6 @@ namespace IW4MAdmin.Application Console.WriteLine($" Version {Utilities.GetVersionAsString()}"); Console.WriteLine("====================================================="); - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("!!!! IMPORTANT !!!!"); - Console.WriteLine("The next update of IW4MAdmin will require .NET 8."); - Console.WriteLine("This is a breaking change!"); - Console.WriteLine( - "Please update the ASP.NET Core Runtime: https://dotnet.microsoft.com/en-us/download/dotnet/8.0"); - Console.WriteLine("!!!!!!!!!!!!!!!!!!!"); - Console.ForegroundColor = ConsoleColor.Gray; - await LaunchAsync(); } @@ -451,12 +443,12 @@ namespace IW4MAdmin.Application var masterUri = Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl; - var httpClient = new HttpClient + var httpClient = new HttpClient(new HttpClientHandler {AllowAutoRedirect = true}) { BaseAddress = masterUri, Timeout = TimeSpan.FromSeconds(15) }; - var masterRestClient = RestClient.For(httpClient); + var masterRestClient = RestService.For(httpClient); var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig); if (appConfig == null) @@ -469,10 +461,7 @@ namespace IW4MAdmin.Application // register override level names foreach (var (key, value) in appConfig.OverridePermissionLevelNames) { - if (!Utilities.PermissionLevelOverrides.ContainsKey(key)) - { - Utilities.PermissionLevelOverrides.Add(key, value); - } + Utilities.PermissionLevelOverrides.TryAdd(key, value); } // build the dependency list @@ -539,6 +528,7 @@ namespace IW4MAdmin.Application .AddSingleton(new ConfigurationWatcher()) .AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>)) .AddSingleton() + .AddSingleton() .AddSingleton(translationLookup) .AddDatabaseContextOptions(appConfig); diff --git a/Application/Misc/MasterCommunication.cs b/Application/Misc/MasterCommunication.cs index 76fc0904..86adc1a6 100644 --- a/Application/Misc/MasterCommunication.cs +++ b/Application/Misc/MasterCommunication.cs @@ -1,5 +1,4 @@ using IW4MAdmin.Application.API.Master; -using RestEase; using SharedLibraryCore; using SharedLibraryCore.Configuration; using SharedLibraryCore.Helpers; @@ -9,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Refit; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.Misc @@ -28,6 +28,7 @@ namespace IW4MAdmin.Application.Misc private readonly int _apiVersion = 1; private bool _firstHeartBeat = true; private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30); + private string _authorizationToken; public MasterCommunication(ILogger logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager) { @@ -128,7 +129,7 @@ namespace IW4MAdmin.Application.Misc Id = _appConfig.Id }); - _apiInstance.AuthorizationToken = $"Bearer {token.AccessToken}"; + _authorizationToken = $"Bearer {token.AccessToken}"; } var instance = new ApiInstance @@ -153,22 +154,22 @@ namespace IW4MAdmin.Application.Misc WebfrontUrl = _appConfig.WebfrontUrl }; - Response response; + IApiResponse response; if (_firstHeartBeat) { - response = await _apiInstance.AddInstance(instance); + response = await _apiInstance.AddInstance(instance, _authorizationToken); } else { - response = await _apiInstance.UpdateInstance(instance.Id, instance); + response = await _apiInstance.UpdateInstance(instance.Id, instance, _authorizationToken); _firstHeartBeat = false; } - if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK) + if (response.StatusCode != System.Net.HttpStatusCode.OK) { - _logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.ResponseMessage.StatusCode, response.StringContent); + _logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.StatusCode, response.Error?.Content); } } } diff --git a/Application/Misc/RemoteAssemblyHandler.cs b/Application/Misc/RemoteAssemblyHandler.cs index 6b7671b1..afe2ec93 100644 --- a/Application/Misc/RemoteAssemblyHandler.cs +++ b/Application/Misc/RemoteAssemblyHandler.cs @@ -55,7 +55,7 @@ namespace IW4MAdmin.Application.Misc var decryptedContent = new byte[encryptedContent.Length]; var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512); - var encryption = new AesGcm(keyGen.GetBytes(KeyLength)); + var encryption = new AesGcm(keyGen.GetBytes(KeyLength),TagLength); try { diff --git a/Application/Misc/SerializationHelpers.cs b/Application/Misc/SerializationHelpers.cs index bfb7e0b9..9d8a5f03 100644 --- a/Application/Misc/SerializationHelpers.cs +++ b/Application/Misc/SerializationHelpers.cs @@ -1,150 +1,255 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SharedLibraryCore; +using SharedLibraryCore; using SharedLibraryCore.Database.Models; using System; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using Data.Models; using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.GameEvent; -namespace IW4MAdmin.Application.Misc +namespace IW4MAdmin.Application.Misc; + +public class IPAddressConverter : JsonConverter { - class IPAddressConverter : JsonConverter + public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override bool CanConvert(Type objectType) - { - return (objectType == typeof(IPAddress)); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(value.ToString()); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return IPAddress.Parse((string)reader.Value); - } + var ipAddressString = reader.GetString(); + return IPAddress.Parse(ipAddressString); } - class IPEndPointConverter : JsonConverter + public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options) { - public override bool CanConvert(Type objectType) - { - return (objectType == typeof(IPEndPoint)); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - IPEndPoint ep = (IPEndPoint)value; - JObject jo = new JObject(); - jo.Add("Address", JToken.FromObject(ep.Address, serializer)); - jo.Add("Port", ep.Port); - jo.WriteTo(writer); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - JObject jo = JObject.Load(reader); - IPAddress address = jo["Address"].ToObject(serializer); - int port = (int)jo["Port"]; - return new IPEndPoint(address, port); - } - } - - class ClientEntityConverter : JsonConverter - { - public override bool CanConvert(Type objectType) => objectType == typeof(EFClient); - - public override object ReadJson(JsonReader reader, Type objectType,object existingValue, JsonSerializer serializer) - { - if (reader.Value == null) - { - return null; - } - - var jsonObject = JObject.Load(reader); - - return new EFClient - { - NetworkId = (long)jsonObject["NetworkId"], - ClientNumber = (int)jsonObject["ClientNumber"], - State = Enum.Parse(jsonObject["state"].ToString()), - CurrentAlias = new EFAlias() - { - IPAddress = (int?)jsonObject["IPAddress"], - Name = jsonObject["Name"].ToString() - } - }; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var client = value as EFClient; - var jsonObject = new JObject - { - { "NetworkId", client.NetworkId }, - { "ClientNumber", client.ClientNumber }, - { "IPAddress", client.CurrentAlias?.IPAddress }, - { "Name", client.CurrentAlias?.Name }, - { "State", (int)client.State } - }; - - jsonObject.WriteTo(writer); - } - } - - class GameEventConverter : JsonConverter - { - public override bool CanConvert(Type objectType) =>objectType == typeof(GameEvent); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var jsonObject = JObject.Load(reader); - - return new GameEvent - { - Type = Enum.Parse(jsonObject["Type"].ToString()), - Subtype = jsonObject["Subtype"]?.ToString(), - Source = Enum.Parse(jsonObject["Source"].ToString()), - RequiredEntity = Enum.Parse(jsonObject["RequiredEntity"].ToString()), - Data = jsonObject["Data"].ToString(), - Message = jsonObject["Message"].ToString(), - GameTime = (int?)jsonObject["GameTime"], - Origin = jsonObject["Origin"]?.ToObject(serializer), - Target = jsonObject["Target"]?.ToObject(serializer), - ImpersonationOrigin = jsonObject["ImpersonationOrigin"]?.ToObject(serializer), - IsRemote = (bool)jsonObject["IsRemote"], - Extra = null, // fix - Time = (DateTime)jsonObject["Time"], - IsBlocking = (bool)jsonObject["IsBlocking"] - }; - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - var gameEvent = value as GameEvent; - - var jsonObject = new JObject - { - { "Type", (int)gameEvent.Type }, - { "Subtype", gameEvent.Subtype }, - { "Source", (int)gameEvent.Source }, - { "RequiredEntity", (int)gameEvent.RequiredEntity }, - { "Data", gameEvent.Data }, - { "Message", gameEvent.Message }, - { "GameTime", gameEvent.GameTime }, - { "Origin", gameEvent.Origin != null ? JToken.FromObject(gameEvent.Origin, serializer) : null }, - { "Target", gameEvent.Target != null ? JToken.FromObject(gameEvent.Target, serializer) : null }, - { "ImpersonationOrigin", gameEvent.ImpersonationOrigin != null ? JToken.FromObject(gameEvent.ImpersonationOrigin, serializer) : null}, - { "IsRemote", gameEvent.IsRemote }, - { "Extra", gameEvent.Extra?.ToString() }, - { "Time", gameEvent.Time }, - { "IsBlocking", gameEvent.IsBlocking } - }; - - jsonObject.WriteTo(writer); - } + writer.WriteStringValue(value.ToString()); + } +} + +public class IPEndPointConverter : JsonConverter +{ + public override IPEndPoint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + IPAddress address = null; + var port = 0; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propertyName = reader.GetString(); + reader.Read(); + switch (propertyName) + { + case "Address": + var addressString = reader.GetString(); + address = IPAddress.Parse(addressString); + break; + case "Port": + port = reader.GetInt32(); + break; + } + } + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + } + + return new IPEndPoint(address, port); + } + + public override void Write(Utf8JsonWriter writer, IPEndPoint value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString("Address", value.Address.ToString()); + writer.WriteNumber("Port", value.Port); + writer.WriteEndObject(); + } +} + +public class ClientEntityConverter : JsonConverter +{ + public override EFClient Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + long networkId = default; + int clientNumber = default; + ClientState state = default; + var currentAlias = new EFAlias(); + int? ipAddress = null; + string name = null; + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propertyName = reader.GetString(); + reader.Read(); // Advance to the value. + switch (propertyName) + { + case "NetworkId": + networkId = reader.GetInt64(); + break; + case "ClientNumber": + clientNumber = reader.GetInt32(); + break; + case "State": + state = (ClientState)reader.GetInt32(); + break; + case "IPAddress": + ipAddress = reader.TokenType != JsonTokenType.Null ? reader.GetInt32() : null; + break; + case "Name": + name = reader.GetString(); + break; + } + } + } + + currentAlias.IPAddress = ipAddress; + currentAlias.Name = name; + + return new EFClient + { + NetworkId = networkId, + ClientNumber = clientNumber, + State = state, + CurrentAlias = currentAlias + }; + } + + public override void Write(Utf8JsonWriter writer, EFClient value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WriteNumber("NetworkId", value.NetworkId); + writer.WriteNumber("ClientNumber", value.ClientNumber); + writer.WriteString("State", value.State.ToString()); + + if (value.CurrentAlias != null) + { + writer.WriteNumber("IPAddress", value.CurrentAlias.IPAddress ?? 0); + writer.WriteString("Name", value.CurrentAlias.Name); + } + else + { + writer.WriteNull("IPAddress"); + writer.WriteNull("Name"); + } + + writer.WriteEndObject(); + } +} + +public class GameEventConverter : JsonConverter +{ + public override GameEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + var gameEvent = new GameEvent(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propertyName = reader.GetString(); + reader.Read(); + switch (propertyName) + { + case "Type": + gameEvent.Type = (EventType)reader.GetInt32(); + break; + case "Subtype": + gameEvent.Subtype = reader.GetString(); + break; + case "Source": + gameEvent.Source = (EventSource)reader.GetInt32(); + break; + case "RequiredEntity": + gameEvent.RequiredEntity = (EventRequiredEntity)reader.GetInt32(); + break; + case "Data": + gameEvent.Data = reader.GetString(); + break; + case "Message": + gameEvent.Message = reader.GetString(); + break; + case "GameTime": + gameEvent.GameTime = reader.TokenType != JsonTokenType.Null ? reader.GetInt32() : null; + break; + case "Origin": + gameEvent.Origin = JsonSerializer.Deserialize(ref reader, options); + break; + case "Target": + gameEvent.Target = JsonSerializer.Deserialize(ref reader, options); + break; + case "ImpersonationOrigin": + gameEvent.ImpersonationOrigin = JsonSerializer.Deserialize(ref reader, options); + break; + case "IsRemote": + gameEvent.IsRemote = reader.GetBoolean(); + break; + case "Time": + gameEvent.Time = reader.GetDateTime(); + break; + case "IsBlocking": + gameEvent.IsBlocking = reader.GetBoolean(); + break; + } + } + } + + return gameEvent; + } + + public override void Write(Utf8JsonWriter writer, GameEvent value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WriteNumber("Type", (int)value.Type); + writer.WriteString("Subtype", value.Subtype); + writer.WriteNumber("Source", (int)value.Source); + writer.WriteNumber("RequiredEntity", (int)value.RequiredEntity); + writer.WriteString("Data", value.Data); + writer.WriteString("Message", value.Message); + if (value.GameTime.HasValue) + { + writer.WriteNumber("GameTime", value.GameTime.Value); + } + else + { + writer.WriteNull("GameTime"); + } + + if (value.Origin != null) + { + writer.WritePropertyName("Origin"); + JsonSerializer.Serialize(writer, value.Origin, options); + } + + if (value.Target != null) + { + writer.WritePropertyName("Target"); + JsonSerializer.Serialize(writer, value.Target, options); + } + + if (value.ImpersonationOrigin != null) + { + writer.WritePropertyName("ImpersonationOrigin"); + JsonSerializer.Serialize(writer, value.ImpersonationOrigin, options); + } + + writer.WriteBoolean("IsRemote", value.IsRemote); + writer.WriteString("Time", value.Time.ToString("o")); + writer.WriteBoolean("IsBlocking", value.IsBlocking); + + writer.WriteEndObject(); } } diff --git a/Application/Plugin/PluginImporter.cs b/Application/Plugin/PluginImporter.cs index b3292096..df3a54f6 100644 --- a/Application/Plugin/PluginImporter.cs +++ b/Application/Plugin/PluginImporter.cs @@ -167,7 +167,7 @@ namespace IW4MAdmin.Application.Plugin try { _pluginSubscription ??= _masterApi - .GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result; + .GetPluginSubscription(_appConfig.Id, _appConfig.SubscriptionId).Result; return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription .Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray()); @@ -185,7 +185,7 @@ namespace IW4MAdmin.Application.Plugin try { _pluginSubscription ??= _masterApi - .GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result; + .GetPluginSubscription(_appConfig.Id, _appConfig.SubscriptionId).Result; return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription .Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray()); diff --git a/Application/Plugin/Script/ScriptPlugin.cs b/Application/Plugin/Script/ScriptPlugin.cs index b5da929f..49822c11 100644 --- a/Application/Plugin/Script/ScriptPlugin.cs +++ b/Application/Plugin/Script/ScriptPlugin.cs @@ -23,6 +23,7 @@ using SharedLibraryCore.Database.Models; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Interfaces; using ILogger = Microsoft.Extensions.Logging.ILogger; +using Reference = Data.Models.Reference; namespace IW4MAdmin.Application.Plugin.Script { diff --git a/Application/Plugin/Script/ScriptPluginV2.cs b/Application/Plugin/Script/ScriptPluginV2.cs index 26edcaa2..8ac8b2a6 100644 --- a/Application/Plugin/Script/ScriptPluginV2.cs +++ b/Application/Plugin/Script/ScriptPluginV2.cs @@ -26,6 +26,7 @@ using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces.Events; using ILogger = Microsoft.Extensions.Logging.ILogger; using JavascriptEngine = Jint.Engine; +using Reference = Data.Models.Reference; namespace IW4MAdmin.Application.Plugin.Script; diff --git a/Application/QueryHelpers/ClientResourceQueryHelper.cs b/Application/QueryHelpers/ClientResourceQueryHelper.cs index f9bff938..d760391a 100644 --- a/Application/QueryHelpers/ClientResourceQueryHelper.cs +++ b/Application/QueryHelpers/ClientResourceQueryHelper.cs @@ -91,9 +91,9 @@ public class ClientResourceQueryHelper : IResourceQueryHelper clientAlias.Key.LastConnection) : iqGroupedClientAliases.OrderBy(clientAlias => clientAlias.Key.LastConnection); - var clientIds = iqGroupedClientAliases.Select(g => g.Key.ClientId) + var clientIds = await iqGroupedClientAliases.Select(g => g.Key.ClientId) .Skip(query.Offset) - .Take(query.Count); + .Take(query.Count).ToListAsync(); // todo: this change was for a pomelo limitation and may be addressed in future version // this pulls in more records than we need, but it's more efficient than ordering grouped entities var clientLookups = await clientAliases diff --git a/Application/RConParsers/BaseRConParser.cs b/Application/RConParsers/BaseRConParser.cs index cf773dd1..ba238e6e 100644 --- a/Application/RConParsers/BaseRConParser.cs +++ b/Application/RConParsers/BaseRConParser.cs @@ -175,6 +175,7 @@ namespace IW4MAdmin.Application.RConParsers return new StatusResponse { + RawResponse = response, Clients = ClientsFromStatus(response).ToArray(), Map = GetValueFromStatus(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus), GameType = GetValueFromStatus(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus), diff --git a/Application/RConParsers/StatusResponse.cs b/Application/RConParsers/StatusResponse.cs index bd0a52b9..fdf21521 100644 --- a/Application/RConParsers/StatusResponse.cs +++ b/Application/RConParsers/StatusResponse.cs @@ -6,10 +6,11 @@ namespace IW4MAdmin.Application.RConParsers /// public class StatusResponse : IStatusResponse { - public string Map { get; set; } - public string GameType { get; set; } - public string Hostname { get; set; } - public int? MaxClients { get; set; } - public EFClient[] Clients { get; set; } + public string Map { get; init; } + public string GameType { get; init; } + public string Hostname { get; init; } + public int? MaxClients { get; init; } + public EFClient[] Clients { get; init; } + public string[] RawResponse { get; set; } } -} \ No newline at end of file +} diff --git a/Data/Data.csproj b/Data/Data.csproj index 75cbf585..9944d083 100644 --- a/Data/Data.csproj +++ b/Data/Data.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Debug;Release;Prerelease AnyCPU RaidMax.IW4MAdmin.Data @@ -9,16 +9,20 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all runtime; build; native; contentfiles - - - - + + + + diff --git a/Data/Models/Vector3.cs b/Data/Models/Vector3.cs index 991a8385..12b20f27 100644 --- a/Data/Models/Vector3.cs +++ b/Data/Models/Vector3.cs @@ -1,12 +1,13 @@ using System; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; + // ReSharper disable CompareOfFloatsByEqualityOperator #pragma warning disable CS0659 namespace Data.Models { - public class Vector3 + public class Vector3 : IParsable { [Key] public int Vector3Id { get; set; } public float X { get; protected set; } @@ -78,7 +79,7 @@ namespace Data.Models return Math.Sqrt((dx * dx) + (dy * dy)); } - + public static double ViewAngleDistance(Vector3 a, Vector3 b, Vector3 c) { double dabX = Math.Abs(a.X - b.X); @@ -111,5 +112,30 @@ namespace Data.Models public double Magnitude() => Math.Sqrt((X * X) + (Y * Y) + (Z * Z)); public double AngleBetween(Vector3 a) => Math.Acos(this.DotProduct(a) / (a.Magnitude() * this.Magnitude())); + + public static Vector3 Parse(string s, IFormatProvider provider) + { + return Parse(s); + } + + public static bool TryParse(string s, IFormatProvider provider, out Vector3 result) + { + result = new Vector3(); + + try + { + var parsed = Parse(s); + result.X = parsed.X; + result.Y = parsed.Y; + result.Z = parsed.Z; + return true; + } + catch + { + // ignored + } + + return false; + } } } diff --git a/DeploymentFiles/PostPublish.ps1 b/DeploymentFiles/PostPublish.ps1 deleted file mode 100644 index 0bcc9bbd..00000000 --- a/DeploymentFiles/PostPublish.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -param ( - [string]$PublishDir = $(throw "-PublishDir is required.") -) - -md -Force ("{0}\Localization" -f $PublishDir) - -$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 $PublishDir, $localization - $response = Invoke-WebRequest $url -UseBasicParsing - Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8 -} - -$versionInfo = (Get-Command ("{0}\IW4MAdmin.exe" -f $PublishDir)).FileVersionInfo -$json = @{ -Major = $versionInfo.ProductMajorPart -Minor = $versionInfo.ProductMinorPart -Build = $versionInfo.ProductBuildPart -Revision = $versionInfo.ProductPrivatePart -} -$json | ConvertTo-Json | Out-File -FilePath ("{0}\VersionInformation.json" -f $PublishDir) -Encoding ASCII diff --git a/DeploymentFiles/deployment-pipeline.yml b/DeploymentFiles/deployment-pipeline.yml deleted file mode 100644 index d684acae..00000000 --- a/DeploymentFiles/deployment-pipeline.yml +++ /dev/null @@ -1,264 +0,0 @@ -name: '$(Date:yyyy.M.d)$(Rev:.r)' - -trigger: - batch: true - branches: - include: - - release/pre - - master - - develop - paths: - exclude: - - '**/*.yml' - - '*.yml' - -pr: none - -pool: - vmImage: 'windows-2022' - -variables: - solution: 'IW4MAdmin.sln' - buildPlatform: 'Any CPU' - outputFolder: '$(Build.ArtifactStagingDirectory)\Publish\$(buildConfiguration)' - releaseType: verified - buildConfiguration: Stable - isPreRelease: false - -jobs: - - job: Build_Deploy - steps: - - task: UseDotNet@2 - displayName: 'Install .NET Core 6 SDK' - inputs: - packageType: 'sdk' - version: '6.0.x' - includePreviewVersions: true - - - task: NuGetToolInstaller@1 - - - task: PowerShell@2 - displayName: 'Setup Pre-Release configuration' - condition: or(eq(variables['Build.SourceBranch'], 'refs/heads/release/pre'), eq(variables['Build.SourceBranch'], 'refs/heads/develop')) - inputs: - targetType: 'inline' - script: | - echo '##vso[task.setvariable variable=releaseType]prerelease' - echo '##vso[task.setvariable variable=buildConfiguration]Prerelease' - echo '##vso[task.setvariable variable=isPreRelease]true' - failOnStderr: true - - - task: NuGetCommand@2 - displayName: 'Restore nuget packages' - inputs: - restoreSolution: '$(solution)' - - - task: PowerShell@2 - displayName: 'Preload external resources' - inputs: - targetType: 'inline' - script: | - Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)' - md -Force lib\open-iconic\font\css - wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss - cd lib\open-iconic\font\css - (Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot' - - - task: VSBuild@1 - displayName: 'Build projects' - inputs: - solution: '$(solution)' - msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber) /p:PackageVersion=$(Build.BuildNumber)' - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' - - - task: PowerShell@2 - displayName: 'Bundle JS Files' - inputs: - targetType: 'inline' - script: | - Write-Host 'Getting dotnet bundle' - wget http://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip -o $(Build.Repository.LocalPath)\dotnet-bundle.zip - Write-Host 'Unzipping download' - Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath) - Write-Host 'Executing dotnet-bundle' - $(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json - $(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore' - - - task: DotNetCoreCLI@2 - displayName: 'Publish projects' - inputs: - command: 'publish' - publishWebProjects: false - projects: | - **/WebfrontCore.csproj - **/Application.csproj - arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Build.BuildNumber)' - zipAfterPublish: false - modifyOutputPath: false - - - task: PowerShell@2 - displayName: 'Run publish script 1' - inputs: - filePath: 'DeploymentFiles/PostPublish.ps1' - arguments: '$(outputFolder)' - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)' - - - task: BatchScript@1 - displayName: 'Run publish script 2' - inputs: - filename: 'Application\BuildScripts\PostPublish.bat' - workingFolder: '$(Build.Repository.LocalPath)' - arguments: '$(outputFolder) $(Build.Repository.LocalPath)' - failOnStandardError: true - - - task: PowerShell@2 - displayName: 'Download dos2unix for line endings' - inputs: - targetType: 'inline' - script: 'wget https://raidmax.org/downloads/dos2unix.exe' - failOnStderr: true - workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - - - task: CmdLine@2 - displayName: 'Convert Linux start script line endings' - inputs: - script: | - echo changing to encoding for linux start script - dos2unix $(outputFolder)\StartIW4MAdmin.sh - dos2unix $(outputFolder)\UpdateIW4MAdmin.sh - echo creating website version filename - @echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt - workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' - - - task: CopyFiles@2 - displayName: 'Move script plugins into publish directory' - inputs: - SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins' - Contents: '*.js' - TargetFolder: '$(outputFolder)\Plugins' - - - task: CopyFiles@2 - displayName: 'Move binary plugins into publish directory' - inputs: - SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\' - Contents: '*.dll' - TargetFolder: '$(outputFolder)\Plugins' - - - task: CmdLine@2 - displayName: 'Move webfront resources into publish directory' - inputs: - script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot' - workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins' - failOnStderr: true - - - task: CmdLine@2 - displayName: 'Move gamescript files into publish directory' - inputs: - script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' - workingDirectory: '$(Build.Repository.LocalPath)' - failOnStderr: true - - - task: ArchiveFiles@2 - displayName: 'Generate final zip file' - inputs: - rootFolderOrFile: '$(outputFolder)' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' - replaceExistingArchive: true - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip' - artifact: 'IW4MAdmin-$(Build.BuildNumber).zip' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish artifact for analysis' - inputs: - targetPath: '$(outputFolder)' - artifact: 'IW4MAdmin.$(buildConfiguration)' - publishLocation: 'pipeline' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish nuget package artifact' - inputs: - targetPath: '$(Build.Repository.LocalPath)/SharedLibraryCore/bin/$(buildConfiguration)/RaidMax.IW4MAdmin.SharedLibraryCore.$(Build.BuildNumber).nupkg' - artifact: 'SharedLibraryCore.$(Build.BuildNumber).nupkg' - publishLocation: 'pipeline' - - - task: FtpUpload@2 - condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') - displayName: 'Upload zip file to website' - inputs: - credentialsOption: 'inputs' - serverUrl: '$(FTPUrl)' - username: '$(FTPUsername)' - password: '$(FTPPassword)' - rootDirectory: '$(Build.ArtifactStagingDirectory)' - filePatterns: '*.zip' - remoteDirectory: 'IW4MAdmin/Download' - clean: false - cleanContents: false - preservePaths: false - trustSSL: false - - - task: FtpUpload@2 - condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') - displayName: 'Upload version info to website' - inputs: - credentialsOption: 'inputs' - serverUrl: '$(FTPUrl)' - username: '$(FTPUsername)' - password: '$(FTPPassword)' - rootDirectory: '$(Build.ArtifactStagingDirectory)' - filePatterns: 'version_$(releaseType).txt' - remoteDirectory: 'IW4MAdmin' - clean: false - cleanContents: false - preservePaths: false - trustSSL: false - - - task: GitHubRelease@1 - condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') - displayName: 'Make GitHub release' - inputs: - gitHubConnection: 'github.com_RaidMax' - repositoryName: 'RaidMax/IW4M-Admin' - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: 'userSpecifiedTag' - tag: '$(Build.BuildNumber)-$(releaseType)' - title: 'IW4MAdmin $(Build.BuildNumber) ($(releaseType))' - assets: '$(Build.ArtifactStagingDirectory)/*.zip' - isPreRelease: $(isPreRelease) - releaseNotesSource: 'inline' - releaseNotesInline: 'Automated rolling release - changelog below. [Updating Instructions](https://github.com/RaidMax/IW4M-Admin/wiki/Getting-Started#updating)' - changeLogCompareToRelease: 'lastNonDraftRelease' - changeLogType: 'commitBased' - - - task: PowerShell@2 - condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop') - displayName: 'Update master version' - inputs: - targetType: 'inline' - script: | - $payload = @{ - 'current-version-$(releaseType)' = '$(Build.BuildNumber)' - 'jwt-secret' = '$(JWTSecret)' - } | ConvertTo-Json - - - $params = @{ - Uri = 'http://api.raidmax.org:5000/version' - Method = 'POST' - Body = $payload - ContentType = 'application/json' - } - - Invoke-RestMethod @params diff --git a/DeploymentFiles/nuget-pipeline.yml b/DeploymentFiles/nuget-pipeline.yml deleted file mode 100644 index ff7ae71e..00000000 --- a/DeploymentFiles/nuget-pipeline.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: '$(Date:yyyy.M.d)$(Rev:.r)' - -pr: none - -pool: - vmImage: 'windows-2022' - -variables: - buildPlatform: 'Any CPU' - outputFolder: '$(Build.ArtifactStagingDirectory)\Publish\$(buildConfiguration)' - releaseType: verified - buildConfiguration: Stable - isPreRelease: false - -jobs: - - job: Build_Pack - steps: - - task: PowerShell@2 - displayName: 'Setup Build configuration' - condition: or(eq(variables['Build.SourceBranch'], 'refs/heads/release/pre'), eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/chore/nuget-pipeline')) - inputs: - targetType: 'inline' - script: | - echo '##vso[task.setvariable variable=releaseType]prerelease' - echo '##vso[task.setvariable variable=buildConfiguration]Prerelease' - echo '##vso[task.setvariable variable=isPreRelease]true' - failOnStderr: true - - - task: DotNetCoreCLI@2 - displayName: 'Build Data' - inputs: - command: 'build' - projects: '**/Data.csproj' - arguments: '-c $(buildConfiguration)' - - - task: DotNetCoreCLI@2 - displayName: 'Build SLC' - inputs: - command: 'build' - projects: '**/SharedLibraryCore.csproj' - arguments: '-c $(buildConfiguration) /p:Version=$(Build.BuildNumber)' - - - task: DotNetCoreCLI@2 - displayName: 'Pack SLC' - inputs: - command: 'pack' - packagesToPack: '**/SharedLibraryCore.csproj' - versioningScheme: 'byBuildNumber' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish nuget package artifact' - inputs: - targetPath: 'D:\a\1\a\RaidMax.IW4MAdmin.SharedLibraryCore.$(Build.BuildNumber).nupkg' - artifact: 'SharedLibraryCore.$(Build.BuildNumber).nupkg' - publishLocation: 'pipeline' diff --git a/GameFiles/GameInterface/_integration_iw6.gsc b/GameFiles/GameInterface/_integration_iw6.gsc new file mode 100644 index 00000000..f98bd9ca --- /dev/null +++ b/GameFiles/GameInterface/_integration_iw6.gsc @@ -0,0 +1,58 @@ +Init() +{ + thread Setup(); +} + +Setup() +{ + level endon( "game_ended" ); + waittillframeend; + + level waittill( level.notifyTypes.sharedFunctionsInitialized ); + + scripts\_integration_base::RegisterLogger( ::Log2Console ); + + level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired; + level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper; + level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper; + level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper; + level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper; + level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout; + + level notify( level.notifyTypes.gameFunctionsInitialized ); +} + +GetTotalShotsFired() +{ + return maps\mp\_utility::getPlayerStat( "mostshotsfired" ); +} + +SetDvarIfUninitializedWrapper( dvar, value ) +{ + SetDvarIfUninitialized( dvar, value ); +} + +WaitillNotifyOrTimeoutWrapper( _notify, timeout ) +{ + common_scripts\utility::waittill_notify_or_timeout( _notify, timeout ); +} + +Log2Console( logLevel, message ) +{ + Print( "[" + logLevel + "] " + message + "\n" ); +} + +IsBotWrapper( client ) +{ + return IsBot( client ); +} + +GetXuidWrapper() +{ + return self GetXUID(); +} + +WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 ) +{ + return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 ); +} diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 87038b44..bf0f6aeb 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -6,13 +6,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}" ProjectSection(SolutionItems) = preProject - DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml - DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1 README.md = README.md version.txt = version.txt DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1 DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh - DeploymentFiles\nuget-pipeline.yml = DeploymentFiles\nuget-pipeline.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}" @@ -112,6 +109,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto IW5", "Pluto IW5", "{ GameFiles\AntiCheat\IW5\README.MD = GameFiles\AntiCheat\IW5\README.MD EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GithubActions", "GithubActions", "{DCCEED9F-816E-4595-8B74-D76A77FBE0BE}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build_application.yml = .github\workflows\build_application.yml + .github\workflows\shared_library_nuget.yml = .github\workflows\shared_library_nuget.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -461,6 +464,7 @@ Global {3EA564BD-3AC6-479B-96B6-CB059DCD0C77} = {AB83BAC0-C539-424A-BF00-78487C10753C} {866F453D-BC89-457F-8B55-485494759B31} = {AB83BAC0-C539-424A-BF00-78487C10753C} {603725A4-BC0B-423B-955B-762C89E1C4C2} = {AB83BAC0-C539-424A-BF00-78487C10753C} + {DCCEED9F-816E-4595-8B74-D76A77FBE0BE} = {8C8F3945-0AEF-4949-A1F7-B18E952E50BC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} diff --git a/Integrations/Cod/Integrations.Cod.csproj b/Integrations/Cod/Integrations.Cod.csproj index 7d10e54a..de12b577 100644 --- a/Integrations/Cod/Integrations.Cod.csproj +++ b/Integrations/Cod/Integrations.Cod.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 Integrations.Cod Integrations.Cod Debug;Release;Prerelease @@ -17,7 +17,7 @@ - + diff --git a/Integrations/Source/Integrations.Source.csproj b/Integrations/Source/Integrations.Source.csproj index cd174636..b33da35b 100644 --- a/Integrations/Source/Integrations.Source.csproj +++ b/Integrations/Source/Integrations.Source.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 Integrations.Source Integrations.Source Debug;Release;Prerelease diff --git a/Plugins/AutomessageFeed/AutomessageFeed.csproj b/Plugins/AutomessageFeed/AutomessageFeed.csproj index 1a79d2e5..73ddb14f 100644 --- a/Plugins/AutomessageFeed/AutomessageFeed.csproj +++ b/Plugins/AutomessageFeed/AutomessageFeed.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 true Latest Debug;Release;Prerelease @@ -10,11 +10,7 @@ - + - - - - diff --git a/Plugins/LiveRadar/Controllers/RadarController.cs b/Plugins/LiveRadar/Controllers/RadarController.cs index 397277a2..d711d87a 100644 --- a/Plugins/LiveRadar/Controllers/RadarController.cs +++ b/Plugins/LiveRadar/Controllers/RadarController.cs @@ -3,7 +3,6 @@ using SharedLibraryCore; using SharedLibraryCore.Dtos; using SharedLibraryCore.Interfaces; using System.Linq; -using System.Threading.Tasks; using IW4MAdmin.Plugins.LiveRadar.Configuration; using Microsoft.AspNetCore.Http; @@ -82,7 +81,7 @@ namespace IW4MAdmin.Plugins.LiveRadar.Web.Controllers } var radarInfo = server.GetClientsAsList() - .Select(client => client.GetAdditionalProperty("LiveRadar")).ToList(); + .Select(client => client.GetAdditionalProperty("LiveRadar")).ToList(); return Json(radarInfo); } diff --git a/Plugins/LiveRadar/Events/LiveRadarEvent.cs b/Plugins/LiveRadar/Events/LiveRadarEvent.cs deleted file mode 100644 index 7121e582..00000000 --- a/Plugins/LiveRadar/Events/LiveRadarEvent.cs +++ /dev/null @@ -1,38 +0,0 @@ -using SharedLibraryCore; -using SharedLibraryCore.Database.Models; -using SharedLibraryCore.Interfaces; -using System.Collections.Generic; -using SharedLibraryCore.Events.Game; -using EventGeneratorCallback = System.ValueTuple>; - -namespace IW4MAdmin.Plugins.LiveRadar.Events; - -public class Script : IRegisterEvent -{ - private const string EventLiveRadar = "LiveRadar"; - - private EventGeneratorCallback LiveRadar() - { - return (EventLiveRadar, EventLiveRadar, (eventLine, _, _) => - { - var radarEvent = new LiveRadarEvent - { - Type = GameEvent.EventType.Other, - Subtype = EventLiveRadar, - Origin = new EFClient { NetworkId = 0 }, - ScriptData = eventLine - }; - return radarEvent; - } - ); - } - - public IEnumerable Events => new[] { LiveRadar() }; -} - -public class LiveRadarEvent : GameScriptEvent -{ -} diff --git a/Plugins/LiveRadar/Events/LiveRadarScriptEvent.cs b/Plugins/LiveRadar/Events/LiveRadarScriptEvent.cs new file mode 100644 index 00000000..4522d04c --- /dev/null +++ b/Plugins/LiveRadar/Events/LiveRadarScriptEvent.cs @@ -0,0 +1,19 @@ +using Data.Models; +using SharedLibraryCore.Events.Game; + +namespace IW4MAdmin.Plugins.LiveRadar.Events; + +public class LiveRadarScriptEvent : GameScriptEvent +{ + public string Name { get; set; } + public Vector3 Location { get; set; } + public Vector3 ViewAngles { get; set; } + public string Team { get; set; } + public int Kills { get; set; } + public int Deaths { get; set; } + public int Score { get; set; } + public string Weapon { get; set; } + public int Health { get; set; } + public bool IsAlive { get; set; } + public int PlayTime { get; set; } +} diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index a5a50206..403ebd2d 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -1,10 +1,10 @@  - net6.0 + net8.0 true true - false + false false true 0.1.0.0 @@ -16,11 +16,7 @@ - + - - - - diff --git a/Plugins/LiveRadar/Plugin.cs b/Plugins/LiveRadar/Plugin.cs index 467359b8..61231edb 100644 --- a/Plugins/LiveRadar/Plugin.cs +++ b/Plugins/LiveRadar/Plugin.cs @@ -36,6 +36,9 @@ public class Plugin : IPluginV2 public static void RegisterDependencies(IServiceCollection serviceCollection) { serviceCollection.AddConfiguration(); + + serviceCollection.AddSingleton(); // for identification + serviceCollection.AddTransient(); // for factory } public Plugin(ILogger logger, ApplicationConfiguration appConfig) @@ -51,7 +54,7 @@ public class Plugin : IPluginV2 private Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token) { - if (scriptEvent is not LiveRadarEvent radarEvent) + if (scriptEvent is not LiveRadarScriptEvent radarEvent) { return Task.CompletedTask; } @@ -83,14 +86,15 @@ public class Plugin : IPluginV2 : (originalBotGuid ?? "0").ConvertGuidToLong(NumberStyles.HexNumber); } - var radarUpdate = RadarEvent.Parse(scriptEvent.ScriptData, generatedBotGuid); + var radarDto = RadarDto.FromScriptEvent(radarEvent, generatedBotGuid); + var client = - radarEvent.Owner.ConnectedClients.FirstOrDefault(client => client.NetworkId == radarUpdate.Guid); + radarEvent.Owner.ConnectedClients.FirstOrDefault(client => client.NetworkId == radarDto.Guid); if (client != null) { - radarUpdate.Name = client.Name.StripColors(); - client.SetAdditionalProperty("LiveRadar", radarUpdate); + radarDto.Name = client.Name.StripColors(); + client.SetAdditionalProperty("LiveRadar", radarDto); } } diff --git a/Plugins/LiveRadar/RadarEvent.cs b/Plugins/LiveRadar/RadarDto.cs similarity index 65% rename from Plugins/LiveRadar/RadarEvent.cs rename to Plugins/LiveRadar/RadarDto.cs index bcbe7cea..4608043a 100644 --- a/Plugins/LiveRadar/RadarEvent.cs +++ b/Plugins/LiveRadar/RadarDto.cs @@ -1,13 +1,13 @@ using Data.Models; using SharedLibraryCore; -using System; -using System.Linq; +using IW4MAdmin.Plugins.LiveRadar.Events; + // ReSharper disable CompareOfFloatsByEqualityOperator #pragma warning disable CS0659 namespace IW4MAdmin.Plugins.LiveRadar; -public class RadarEvent +public class RadarDto { public string Name { get; set; } public long Guid { get; set; } @@ -26,7 +26,7 @@ public class RadarEvent public override bool Equals(object obj) { - if (obj is RadarEvent re) + if (obj is RadarDto re) { return re.ViewAngles.X == ViewAngles.X && re.ViewAngles.Y == ViewAngles.Y && @@ -39,23 +39,21 @@ public class RadarEvent return false; } - public static RadarEvent Parse(string input, long generatedBotGuid) + public static RadarDto FromScriptEvent(LiveRadarScriptEvent scriptEvent, long generatedBotGuid) { - var items = input.Split(';').Skip(1).ToList(); - - var parsedEvent = new RadarEvent() + var parsedEvent = new RadarDto { Guid = generatedBotGuid, - Location = Vector3.Parse(items[1]), - ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(), - Team = items[3], - Kills = int.Parse(items[4]), - Deaths = int.Parse(items[5]), - Score = int.Parse(items[6]), - Weapon = items[7], - Health = int.Parse(items[8]), - IsAlive = items[9] == "1", - PlayTime = Convert.ToInt32(items[10]) + Location = scriptEvent.Location, + ViewAngles = scriptEvent.ViewAngles.FixIW4Angles(), + Team = scriptEvent.Team, + Kills = scriptEvent.Kills, + Deaths = scriptEvent.Deaths, + Score = scriptEvent.Score, + Weapon =scriptEvent.Weapon, + Health = scriptEvent.Health, + IsAlive = scriptEvent.IsAlive, + PlayTime = scriptEvent.PlayTime }; return parsedEvent; diff --git a/Plugins/Login/Login.csproj b/Plugins/Login/Login.csproj index b3a92171..5c775b5e 100644 --- a/Plugins/Login/Login.csproj +++ b/Plugins/Login/Login.csproj @@ -2,7 +2,7 @@ Library - net6.0 + net8.0 false @@ -19,11 +19,7 @@ - + - - - - diff --git a/Plugins/Mute/Commands/MuteCommand.cs b/Plugins/Mute/Commands/MuteCommand.cs index 82c4cd7e..0e894f54 100644 --- a/Plugins/Mute/Commands/MuteCommand.cs +++ b/Plugins/Mute/Commands/MuteCommand.cs @@ -20,8 +20,8 @@ public class MuteCommand : Command Permission = EFClient.Permission.Moderator; RequiresTarget = true; SupportedGames = Plugin.SupportedGames; - Arguments = new[] - { + Arguments = + [ new CommandArgument { Name = translationLookup["COMMANDS_ARGS_PLAYER"], @@ -32,7 +32,7 @@ public class MuteCommand : Command Name = translationLookup["COMMANDS_ARGS_REASON"], Required = true } - }; + ]; } public override async Task ExecuteAsync(GameEvent gameEvent) diff --git a/Plugins/Mute/Commands/MuteInfoCommand.cs b/Plugins/Mute/Commands/MuteInfoCommand.cs index 4622a0ce..bba01a9e 100644 --- a/Plugins/Mute/Commands/MuteInfoCommand.cs +++ b/Plugins/Mute/Commands/MuteInfoCommand.cs @@ -21,14 +21,14 @@ public class MuteInfoCommand : Command Permission = EFClient.Permission.Moderator; RequiresTarget = true; SupportedGames = Plugin.SupportedGames; - Arguments = new[] - { + Arguments = + [ new CommandArgument { Name = translationLookup["COMMANDS_ARGS_PLAYER"], Required = true } - }; + ]; } public override async Task ExecuteAsync(GameEvent gameEvent) diff --git a/Plugins/Mute/Commands/TempMuteCommand.cs b/Plugins/Mute/Commands/TempMuteCommand.cs index 69a2999c..9275b107 100644 --- a/Plugins/Mute/Commands/TempMuteCommand.cs +++ b/Plugins/Mute/Commands/TempMuteCommand.cs @@ -7,10 +7,9 @@ using SharedLibraryCore.Interfaces; namespace IW4MAdmin.Plugins.Mute.Commands; -public class TempMuteCommand : Command +public partial class TempMuteCommand : Command { private readonly MuteManager _muteManager; - private const string TempBanRegex = @"([0-9]+\w+)\ (.+)"; public TempMuteCommand(CommandConfiguration config, ITranslationLookup translationLookup, MuteManager muteManager) : base(config, translationLookup) @@ -22,8 +21,8 @@ public class TempMuteCommand : Command Permission = EFClient.Permission.Moderator; RequiresTarget = true; SupportedGames = Plugin.SupportedGames; - Arguments = new[] - { + Arguments = + [ new CommandArgument { Name = translationLookup["COMMANDS_ARGS_PLAYER"], @@ -39,7 +38,7 @@ public class TempMuteCommand : Command Name = translationLookup["COMMANDS_ARGS_REASON"], Required = true } - }; + ]; } public override async Task ExecuteAsync(GameEvent gameEvent) @@ -49,8 +48,8 @@ public class TempMuteCommand : Command gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]); return; } - - var match = Regex.Match(gameEvent.Data, TempBanRegex); + + var match = TempBanRegex().Match(gameEvent.Data); if (match.Success) { var expiration = DateTime.UtcNow + match.Groups[1].ToString().ParseTimespan(); @@ -72,4 +71,7 @@ public class TempMuteCommand : Command gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_BAD_FORMAT"]); } + + [GeneratedRegex(@"([0-9]+\w+)\ (.+)")] + private static partial Regex TempBanRegex(); } diff --git a/Plugins/Mute/Commands/UnmuteCommand.cs b/Plugins/Mute/Commands/UnmuteCommand.cs index e59c4334..04361144 100644 --- a/Plugins/Mute/Commands/UnmuteCommand.cs +++ b/Plugins/Mute/Commands/UnmuteCommand.cs @@ -20,8 +20,8 @@ public class UnmuteCommand : Command Permission = EFClient.Permission.Moderator; RequiresTarget = true; SupportedGames = Plugin.SupportedGames; - Arguments = new[] - { + Arguments = + [ new CommandArgument { Name = translationLookup["COMMANDS_ARGS_PLAYER"], @@ -32,7 +32,7 @@ public class UnmuteCommand : Command Name = translationLookup["COMMANDS_ARGS_REASON"], Required = true } - }; + ]; } public override async Task ExecuteAsync(GameEvent gameEvent) diff --git a/Plugins/Mute/Mute.csproj b/Plugins/Mute/Mute.csproj index 19e6bb03..3341c525 100644 --- a/Plugins/Mute/Mute.csproj +++ b/Plugins/Mute/Mute.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable MrAmos123 @@ -12,10 +12,7 @@ - + - - - - + diff --git a/Plugins/Mute/MuteManager.cs b/Plugins/Mute/MuteManager.cs index 537465b1..e758d5eb 100644 --- a/Plugins/Mute/MuteManager.cs +++ b/Plugins/Mute/MuteManager.cs @@ -10,23 +10,15 @@ using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Plugins.Mute; -public class MuteManager +public class MuteManager( + ILogger logger, + IDatabaseContextFactory databaseContextFactory, + IMetaServiceV2 metaService, + ITranslationLookup translationLookup) { - private readonly IMetaServiceV2 _metaService; - private readonly ITranslationLookup _translationLookup; - private readonly ILogger _logger; - private readonly IDatabaseContextFactory _databaseContextFactory; + private readonly ILogger _logger = logger; private readonly SemaphoreSlim _onMuteAction = new(1, 1); - public MuteManager(ILogger logger, IDatabaseContextFactory databaseContextFactory, - IMetaServiceV2 metaService, ITranslationLookup translationLookup) - { - _logger = logger; - _databaseContextFactory = databaseContextFactory; - _metaService = metaService; - _translationLookup = translationLookup; - } - public static bool IsExpiredMute(MuteStateMeta muteStateMeta) => muteStateMeta.Expiration is not null && muteStateMeta.Expiration < DateTime.UtcNow; @@ -42,7 +34,7 @@ public class MuteManager var muteState = await ReadPersistentDataV1(client); clientMuteMeta = new MuteStateMeta { - Reason = muteState is null ? string.Empty : _translationLookup["PLUGINS_MUTE_MIGRATED"], + Reason = muteState is null ? string.Empty : translationLookup["PLUGINS_MUTE_MIGRATED"], Expiration = muteState switch { null => DateTime.UtcNow, @@ -149,7 +141,7 @@ public class MuteManager private async Task ExpireMutePenalties(EFClient client) { - await using var context = _databaseContextFactory.CreateContext(); + await using var context = databaseContextFactory.CreateContext(); var mutePenalties = await context.Penalties .Where(penalty => penalty.OffenderId == client.ClientId) .Where(penalty => penalty.Type == EFPenalty.PenaltyType.Mute || penalty.Type == EFPenalty.PenaltyType.TempMute) @@ -184,7 +176,7 @@ public class MuteManager } private async Task ReadPersistentDataV1(EFClient client) => TryParse( - (await _metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value, out var muteState) + (await metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value, out var muteState) ? muteState : null; @@ -195,7 +187,7 @@ public class MuteManager if (clientMuteMeta is not null) return clientMuteMeta; // Get meta from database and store in client if exists - clientMuteMeta = await _metaService.GetPersistentMetaValue(Plugin.MuteKey, client.ClientId); + clientMuteMeta = await metaService.GetPersistentMetaValue(Plugin.MuteKey, client.ClientId); if (clientMuteMeta is not null) client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta); return clientMuteMeta; @@ -204,6 +196,6 @@ public class MuteManager private async Task WritePersistentData(EFClient client, MuteStateMeta clientMuteMeta) { client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta); - await _metaService.SetPersistentMetaValue(Plugin.MuteKey, clientMuteMeta, client.ClientId); + await metaService.SetPersistentMetaValue(Plugin.MuteKey, clientMuteMeta, client.ClientId); } } diff --git a/Plugins/Mute/Plugin.cs b/Plugins/Mute/Plugin.cs index fc75ea27..44ca8b2c 100644 --- a/Plugins/Mute/Plugin.cs +++ b/Plugins/Mute/Plugin.cs @@ -21,15 +21,14 @@ public class Plugin : IPluginV2 public const string MuteKey = "IW4MMute"; public static IManager Manager { get; private set; } = null!; - public static Server.Game[] SupportedGames { get; private set; } = Array.Empty(); - private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"}; + public static Server.Game[] SupportedGames { get; private set; } = []; + private static readonly string[] DisabledCommands = [nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"]; private readonly IInteractionRegistration _interactionRegistration; private readonly IRemoteCommandService _remoteCommandService; private readonly MuteManager _muteManager; private const string MuteInteraction = "Webfront::Profile::Mute"; - public Plugin(IInteractionRegistration interactionRegistration, - IRemoteCommandService remoteCommandService, MuteManager muteManager) + public Plugin(IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService, MuteManager muteManager) { _interactionRegistration = interactionRegistration; _remoteCommandService = remoteCommandService; @@ -72,7 +71,7 @@ public class Plugin : IPluginV2 return !DisabledCommands.Contains(command.GetType().Name) && !command.IsBroadcast; }); - _interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, token) => + _interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, _) => { if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value)) { @@ -80,16 +79,16 @@ public class Plugin : IPluginV2 } var clientMuteMetaState = - (await _muteManager.GetCurrentMuteState(new EFClient {ClientId = targetClientId.Value})) + (await _muteManager.GetCurrentMuteState(new EFClient { ClientId = targetClientId.Value })) .MuteState; var server = manager.GetServers().First(); - string GetCommandName(Type commandType) => - manager.Commands.FirstOrDefault(command => command.GetType() == commandType)?.Name ?? ""; - return clientMuteMetaState is MuteState.Unmuted or MuteState.Unmuting ? CreateMuteInteraction(targetClientId.Value, server, GetCommandName) : CreateUnmuteInteraction(targetClientId.Value, server, GetCommandName); + + string GetCommandName(Type commandType) => + manager.Commands.FirstOrDefault(command => command.GetType() == commandType)?.Name ?? string.Empty; }); return Task.CompletedTask; } @@ -108,9 +107,9 @@ public class Plugin : IPluginV2 } var networkIds = updateEvent.Clients.Select(client => client.NetworkId).ToList(); - var ingameClients = updateEvent.Server.ConnectedClients.Where(client => networkIds.Contains(client.NetworkId)); + var inGameClients = updateEvent.Server.ConnectedClients.Where(client => networkIds.Contains(client.NetworkId)); - await Task.WhenAll(ingameClients.Select(async client => + await Task.WhenAll(inGameClients.Select(async client => { var muteMetaUpdate = await _muteManager.GetCurrentMuteState(client); if (!muteMetaUpdate.CommandExecuted) @@ -136,7 +135,7 @@ public class Plugin : IPluginV2 { var muteMetaSay = await _muteManager.GetCurrentMuteState(messageEvent.Origin); - if (muteMetaSay.MuteState == MuteState.Muted) + if (muteMetaSay.MuteState is MuteState.Muted) { // Let the client know when their mute expires. messageEvent.Origin.Tell(Utilities.CurrentLocalization @@ -159,16 +158,16 @@ public class Plugin : IPluginV2 switch (muteMetaJoin) { - case {MuteState: MuteState.Muted}: + case { MuteState: MuteState.Muted }: // Let the client know when their mute expires. state.Client.Tell(Utilities.CurrentLocalization .LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt( - muteMetaJoin is {Expiration: not null} + muteMetaJoin is { Expiration: not null } ? muteMetaJoin.Expiration.Value.HumanizeForCurrentCulture() : Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"], muteMetaJoin.Reason)); break; - case {MuteState: MuteState.Unmuting}: + case { MuteState: MuteState.Unmuting }: // Handle unmute of unmuted players. await _muteManager.Unmute(state.Client.CurrentServer, Utilities.IW4MAdminClient(), state.Client, muteMetaJoin.Reason ?? string.Empty); @@ -190,6 +189,29 @@ public class Plugin : IPluginV2 Values = (Dictionary?)null }; + var presetReasonInput = new + { + Name = "PresetReason", + Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_PRESET_REASON"], + Type = "select", + Values = (Dictionary?)new Dictionary + { + { string.Empty, string.Empty }, + { + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_REASON_ABUSIVE"], + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_REASON_ABUSIVE"] + }, + { + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_REASON_SPAMMING"], + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_REASON_SPAMMING"] + }, + { + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_REASON_OTHER"], + Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_REASON_OTHER"] + } + } + }; + var durationInput = new { Name = "Duration", @@ -197,16 +219,16 @@ public class Plugin : IPluginV2 Type = "select", Values = (Dictionary?)new Dictionary { - {"5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture()}, - {"30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture()}, - {"1h", TimeSpan.FromHours(1).HumanizeForCurrentCulture()}, - {"6h", TimeSpan.FromHours(6).HumanizeForCurrentCulture()}, - {"1d", TimeSpan.FromDays(1).HumanizeForCurrentCulture()}, - {"p", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_SELECTION_PERMANENT"]} + { "5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture() }, + { "30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture() }, + { "1h", TimeSpan.FromHours(1).HumanizeForCurrentCulture() }, + { "6h", TimeSpan.FromHours(6).HumanizeForCurrentCulture() }, + { "1d", TimeSpan.FromDays(1).HumanizeForCurrentCulture() }, + { "p", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_SELECTION_PERMANENT"] } } }; - var inputs = new[] {reasonInput, durationInput}; + var inputs = new[] { reasonInput, presetReasonInput, durationInput }; var inputsJson = JsonSerializer.Serialize(inputs); return new InteractionData @@ -215,10 +237,10 @@ public class Plugin : IPluginV2 Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"], DisplayMeta = "oi-volume-off", ActionPath = "DynamicAction", - ActionMeta = new() + ActionMeta = new Dictionary { - {"InteractionId", MuteInteraction}, - {"Inputs", inputsJson}, + { "InteractionId", MuteInteraction }, + { "Inputs", inputsJson }, { "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"] @@ -227,7 +249,7 @@ public class Plugin : IPluginV2 "Name", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"] }, - {"ShouldRefresh", true.ToString()} + { "ShouldRefresh", true.ToString() } }, MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator, Source = Name, @@ -249,11 +271,14 @@ public class Plugin : IPluginV2 args.Add(duration); } - if (meta.TryGetValue(reasonInput.Name, out var reason)) + var definedReason = meta.TryGetValue(reasonInput.Name, out var reason) ? reason : string.Empty; + if (meta.TryGetValue(presetReasonInput.Name, out var presetReason) && string.IsNullOrWhiteSpace(definedReason)) { - args.Add(reason); + definedReason = presetReason; } + args.Add(definedReason); + var commandResponse = await _remoteCommandService.Execute(originId, targetId, muteCommand, args, server); return string.Join(".", commandResponse.Select(result => result.Response)); @@ -271,21 +296,20 @@ public class Plugin : IPluginV2 Type = "text", }; - var inputs = new[] {reasonInput}; + var inputs = new[] { reasonInput }; var inputsJson = JsonSerializer.Serialize(inputs); return new InteractionData { EntityId = targetClientId, - Name = Utilities.CurrentLocalization.LocalizationIndex[ - "WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"], + Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"], DisplayMeta = "oi-volume-high", ActionPath = "DynamicAction", - ActionMeta = new() + ActionMeta = new Dictionary { - {"InteractionId", MuteInteraction}, - {"Outputs", reasonInput.Name}, - {"Inputs", inputsJson}, + { "InteractionId", MuteInteraction }, + { "Outputs", reasonInput.Name }, + { "Inputs", inputsJson }, { "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex[ @@ -296,7 +320,7 @@ public class Plugin : IPluginV2 Utilities.CurrentLocalization.LocalizationIndex[ "WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"] }, - {"ShouldRefresh", true.ToString()} + { "ShouldRefresh", true.ToString() } }, MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator, Source = Name, diff --git a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj index 8334a992..0818d044 100644 --- a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj +++ b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj @@ -2,7 +2,7 @@ Library - net6.0 + net8.0 RaidMax.IW4MAdmin.Plugins.ProfanityDeterment @@ -16,11 +16,7 @@ - + - - - - diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js index 19cf64db..9e1bb33e 100644 --- a/Plugins/ScriptPlugins/GameInterface.js +++ b/Plugins/ScriptPlugins/GameInterface.js @@ -94,7 +94,7 @@ const plugin = { onServerValueSetCompleted: async function (serverValueEvent) { this.logger.logDebug('Set {dvarName}={dvarValue} success={success} from {server}', serverValueEvent.valueName, serverValueEvent.value, serverValueEvent.success, serverValueEvent.server.id); - + if (serverValueEvent.valueName !== inDvar && serverValueEvent.valueName !== outDvar) { this.logger.logDebug('Ignoring set complete of {name}', serverValueEvent.valueName); return; @@ -124,7 +124,7 @@ const plugin = { // loop restarts this.requestGetDvar(inDvar, serverValueEvent.server); }, - + onServerMonitoringStart: function (monitorStartEvent) { this.initializeServer(monitorStartEvent.server); }, @@ -162,7 +162,7 @@ const plugin = { serverState.enabled = true; serverState.running = true; serverState.initializationInProgress = false; - + // todo: this might not work for all games responseEvent.server.rconParser.configuration.floodProtectInterval = 150; @@ -233,7 +233,7 @@ const plugin = { // todo: refactor to mapping if possible if (event.eventType === 'ClientDataRequested') { - const client = server.getClientByNumber(event.clientNumber); + const client = server.connectedClients[event.clientNumber]; if (client != null) { this.logger.logDebug('Found client {name}', client.name); @@ -269,8 +269,9 @@ const plugin = { } } + let _; if (event.eventType === 'SetClientDataRequested') { - let client = server.getClientByNumber(event.clientNumber); + let client = server.connectedClients[event.clientNumber]; let clientId; if (client != null) { @@ -298,12 +299,12 @@ const plugin = { const parsedValue = parseInt(event.data['value']); const key = event.data['key'].toString(); if (!isNaN(parsedValue)) { - event.data['direction'] = 'up' ? + _ = event.data['direction'] === 'increment' ? (await metaService.incrementPersistentMeta(key, parsedValue, clientId, token)).result : (await metaService.decrementPersistentMeta(key, parsedValue, clientId, token)).result; } } else { - const _ = (await metaService.setPersistentMeta(event.data['key'], event.data['value'], clientId, token)).result; + _ = (await metaService.setPersistentMeta(event.data['key'], event.data['value'], clientId, token)).result; } if (event.data['key'] === 'PersistentClientGuid') { @@ -342,34 +343,34 @@ const plugin = { if (typeof response !== 'string' && !(response instanceof String)) { response = JSON.stringify(response); } - + const max = 10; this.logger.logDebug(`response length ${response.length}`); - + let quoteReplace = '\\"'; // todo: may be more than just T6 if (server.gameCode === 'T6') { quoteReplace = '\\\\"'; } - + let chunks = chunkString(response.replace(/"/gm, quoteReplace).replace(/[\n|\t]/gm, ''), 800); if (chunks.length > max) { this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`); chunks = chunks.slice(0, max); } this.logger.logDebug(`chunk size ${chunks.length}`); - + for (let i = 0; i < chunks.length; i++) { this.sendEventMessage(server, false, 'UrlRequestCompleted', null, null, - null, { entity: event.data.entity, remaining: chunks.length - (i + 1), response: chunks[i]}); + null, {entity: event.data.entity, remaining: chunks.length - (i + 1), response: chunks[i]}); } }); } - + if (event.eventType === 'RegisterCommandRequested') { this.registerDynamicCommand(event); } - + if (event.eventType === 'GetBusModeRequested') { if (event.data?.directory && event.data?.mode) { busMode = event.data.mode; @@ -433,10 +434,10 @@ const plugin = { }); } }); - + return; } - + const serverEvents = importNamespace('SharedLibraryCore.Events.Server'); const requestEvent = new serverEvents.ServerValueRequestEvent(dvarName, server); requestEvent.delayMs = this.config.pollingRate; @@ -467,8 +468,8 @@ const plugin = { requestSetDvar: function (dvarName, dvarValue, server) { const serverState = servers[server.id]; - - if ( busMode === 'file' ) { + + if (busMode === 'file') { this.scriptHelper.requestNotifyAfterDelay(250, async () => { const io = importNamespace('System.IO'); try { @@ -493,7 +494,7 @@ const plugin = { }); } }) - + return; } @@ -526,7 +527,7 @@ const plugin = { } }, - parseUrlRequest: function(event) { + parseUrlRequest: function (event) { const url = event.data?.url; if (url === undefined) { @@ -556,8 +557,8 @@ const plugin = { const script = importNamespace('IW4MAdmin.Application.Plugin.Script'); return new script.ScriptPluginWebRequest(url, body, method, contentType, headerDict); }, - - registerDynamicCommand: function(event) { + + registerDynamicCommand: function (event) { const commandWrapper = { commands: [{ name: event.data['name'] || 'DEFAULT', @@ -571,9 +572,9 @@ const plugin = { if (!validateEnabled(gameEvent.owner, gameEvent.origin)) { return; } - + if (gameEvent.data === '--reload' && gameEvent.origin.level === 'Owner') { - this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, { name: gameEvent.extra.name }); + this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, {name: gameEvent.extra.name}); } else { sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, { args: gameEvent.data @@ -582,7 +583,7 @@ const plugin = { } }] } - + this.scriptHelper.registerDynamicCommand(commandWrapper); } }; @@ -920,6 +921,6 @@ const fileForDvar = (dvar) => { if (dvar === inDvar) { return busFileIn; } - + return busFileOut; } diff --git a/Plugins/ScriptPlugins/ParserS1x.js b/Plugins/ScriptPlugins/ParserS1x.js index 14baa548..e7b5f998 100644 --- a/Plugins/ScriptPlugins/ParserS1x.js +++ b/Plugins/ScriptPlugins/ParserS1x.js @@ -16,7 +16,7 @@ var plugin = { rconParser.Configuration.CommandPrefixes.Kick = 'kickClient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"'; - rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint'; + rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n'; rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)?(.*)$'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$'; rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *'; diff --git a/Plugins/ScriptPlugins/ServerBanner.js b/Plugins/ScriptPlugins/ServerBanner.js index 0beb3da1..374f9a79 100644 --- a/Plugins/ScriptPlugins/ServerBanner.js +++ b/Plugins/ScriptPlugins/ServerBanner.js @@ -9,7 +9,7 @@ const serverOrderCache = []; const plugin = { author: 'RaidMax', - version: '1.0', + version: '1.1', name: 'Server Banner', serviceResolver: null, scriptHelper: null, @@ -26,6 +26,9 @@ const plugin = { this.manager = serviceResolver.resolveService('IManager'); this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']); this.webfrontUrl = serviceResolver.resolveService('ApplicationConfiguration').webfrontUrl; + + this.logger.logInformation('{Name} {Version} by {Author} loaded,', this.name, this.version, + this.author); }, onServerMonitoringStart: function (startEvent) { @@ -123,11 +126,13 @@ const plugin = { }, }; - plugin.manager.getServers().forEach(eachServer => { - if (eachServer.id === serverId) { - server = eachServer; + const servers = plugin.manager.servers; + for (let i = 0; i < servers.length; i++) { + if (servers[i].id === serverId) { + server = servers[i]; + break; } - }); + } if (serverLocationCache[server.listenAddress] === undefined) { plugin.onServerMonitoringStart({ @@ -280,7 +285,7 @@ const plugin = {
-
${server.serverName.stripColors()}
+
${status} @@ -298,6 +303,10 @@ const plugin = {
+ `; } @@ -310,7 +319,7 @@ const plugin = { style="background: url('https://raidmax.org/resources/images/icons/games/${gameCode}.jpg');">
-
${server.serverName.stripColors()}
+
${displayIp}:${server.listenPort}
${server.throttled ? '-' : server.clientNum}/${server.maxClients} Players
@@ -324,6 +333,10 @@ const plugin = { ${status}
+ `; }; @@ -346,22 +359,24 @@ const plugin = { interactionData.scriptAction = (_, __, ___, ____, _____) => { if (Object.keys(serverOrderCache).length === 0) { - plugin.manager.getServers().forEach(server => { + for (let i = 0; i < plugin.manager.servers.length; i++) { + const server = plugin.manager.servers[i]; plugin.onServerMonitoringStart({ server: server }); - }); + } } let response = '
'; Object.keys(serverOrderCache).forEach(key => { const servers = serverOrderCache[key]; - servers.forEach(eachServer => { + for (let i = 0; i < servers.length; i++) { + const eachServer = servers[i]; response += `
${eachServer.gameCode}
- ${eachServer.serverName.stripColors()} +
@@ -387,8 +402,12 @@ const plugin = {
 width="400" height="70" style="border-width: 0; overflow: hidden;">
</iframe>
-
`; - }); +
+ `; + } }); response += ''; diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 7aff2f0c..c8fafb0a 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -2,7 +2,7 @@ Library - net6.0 + net8.0 RaidMax.IW4MAdmin.Plugins.Stats @@ -17,11 +17,7 @@ - + - - - - diff --git a/Plugins/Welcome/Plugin.cs b/Plugins/Welcome/Plugin.cs index e5189f11..30d43870 100644 --- a/Plugins/Welcome/Plugin.cs +++ b/Plugins/Welcome/Plugin.cs @@ -6,8 +6,8 @@ using SharedLibraryCore.Database.Models; using System.Linq; using Microsoft.EntityFrameworkCore; using System.Net.Http; +using System.Text.Json; using System.Threading; -using Newtonsoft.Json.Linq; using Humanizer; using Data.Abstractions; using Data.Models; @@ -108,8 +108,9 @@ public class Plugin : IPluginV2 var response = await wc.GetStringAsync(new Uri( $"http://ip-api.com/json/{ip}?lang={Utilities.CurrentLocalization.LocalizationName.Split("-").First().ToLower()}")); - var responseObj = JObject.Parse(response); - response = responseObj["country"]?.ToString(); + + var json = JsonDocument.Parse(response); + response = json.RootElement.TryGetProperty("country", out var countryElement) ? countryElement.GetString() : null; return string.IsNullOrEmpty(response) ? Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_UNKNOWN_COUNTRY"] diff --git a/Plugins/Welcome/Welcome.csproj b/Plugins/Welcome/Welcome.csproj index de9255ef..11c3a037 100644 --- a/Plugins/Welcome/Welcome.csproj +++ b/Plugins/Welcome/Welcome.csproj @@ -2,7 +2,7 @@ Library - net6.0 + net8.0 RaidMax.IW4MAdmin.Plugins.Welcome @@ -15,12 +15,8 @@ Latest - - - - - + diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs index 4e5c9c16..e8b72a32 100644 --- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs +++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Text.Json.Serialization; using Data.Models.Misc; -using Newtonsoft.Json; using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Interfaces; using static Data.Models.Client.EFClient; @@ -22,7 +22,7 @@ namespace SharedLibraryCore.Configuration public bool EnableWebFront { get; set; } [LocalizedDisplayName("WEBFRONT_CONFIGURATION_BIND_URL")] - public string WebfrontBindUrl { get; set; } + public string WebfrontBindUrl { get; set; } = "http://0.0.0.0:1624"; [ConfigurationOptional] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_MANUAL_URL")] diff --git a/SharedLibraryCore/Configuration/CommandProperties.cs b/SharedLibraryCore/Configuration/CommandProperties.cs index da524910..e56c0e31 100644 --- a/SharedLibraryCore/Configuration/CommandProperties.cs +++ b/SharedLibraryCore/Configuration/CommandProperties.cs @@ -1,41 +1,40 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; +using SharedLibraryCore.Helpers; using static Data.Models.Client.EFClient; using static SharedLibraryCore.Server; -namespace SharedLibraryCore.Configuration +namespace SharedLibraryCore.Configuration; + +/// +/// Config driven command properties +/// +public class CommandProperties { /// - /// Config driven command properties + /// Specifies the command name /// - public class CommandProperties - { - /// - /// Specifies the command name - /// - public string Name { get; set; } + public string Name { get; set; } - /// - /// Alias of this command - /// - public string Alias { get; set; } + /// + /// Alias of this command + /// + public string Alias { get; set; } - /// - /// Specifies the minimum permission level needed to execute the - /// - [JsonConverter(typeof(StringEnumConverter))] - public Permission MinimumPermission { get; set; } + /// + /// Specifies the minimum permission level needed to execute the + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public Permission MinimumPermission { get; set; } - /// - /// Indicates if the command can be run by another user (impersonation) - /// - public bool AllowImpersonation { get; set; } + /// + /// Indicates if the command can be run by another user (impersonation) + /// + public bool AllowImpersonation { get; set; } - /// - /// Specifies the games supporting the functionality of the command - /// - [JsonProperty(ItemConverterType = typeof(StringEnumConverter))] - public Game[] SupportedGames { get; set; } = Array.Empty(); - } + /// + /// Specifies the games supporting the functionality of the command + /// + [JsonConverter(typeof(GameArrayJsonConverter))] + public Game[] SupportedGames { get; set; } = Array.Empty(); } diff --git a/SharedLibraryCore/Configuration/ServerConfiguration.cs b/SharedLibraryCore/Configuration/ServerConfiguration.cs index 0a6da74f..369a3456 100644 --- a/SharedLibraryCore/Configuration/ServerConfiguration.cs +++ b/SharedLibraryCore/Configuration/ServerConfiguration.cs @@ -9,16 +9,9 @@ namespace SharedLibraryCore.Configuration { public class ServerConfiguration : IBaseConfiguration { - private readonly IList _rconParsers; + private readonly IList _rconParsers = new List(); private IRConParser _selectedParser; - public ServerConfiguration() - { - _rconParsers = new List(); - Rules = new string[0]; - AutoMessages = new string[0]; - } - [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_IP")] public string IPAddress { get; set; } @@ -29,10 +22,10 @@ namespace SharedLibraryCore.Configuration public string Password { get; set; } [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_RULES")] - public string[] Rules { get; set; } = new string[0]; + public string[] Rules { get; set; } = []; [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_AUTO_MESSAGES")] - public string[] AutoMessages { get; set; } = new string[0]; + public string[] AutoMessages { get; set; } = []; [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_PATH")] [ConfigurationOptional] @@ -88,7 +81,7 @@ namespace SharedLibraryCore.Configuration var passwords = _selectedParser.TryGetRConPasswords(); if (passwords.Length > 1) { - var (index, value) = + var (index, _) = loc["SETUP_RCON_PASSWORD_PROMPT"].PromptSelection(loc["SETUP_RCON_PASSWORD_MANUAL"], null, passwords.Select(pw => $"{pw.Item1}{(string.IsNullOrEmpty(pw.Item2) ? "" : " " + pw.Item2)}") @@ -113,9 +106,8 @@ namespace SharedLibraryCore.Configuration Password = loc["SETUP_SERVER_RCON"].PromptString(); } - AutoMessages = new string[0]; - Rules = new string[0]; - ManualLogPath = null; + AutoMessages = []; + Rules = []; return this; } diff --git a/SharedLibraryCore/Events/CoreEvent.cs b/SharedLibraryCore/Events/CoreEvent.cs index 17d44b49..1891d64e 100644 --- a/SharedLibraryCore/Events/CoreEvent.cs +++ b/SharedLibraryCore/Events/CoreEvent.cs @@ -6,7 +6,7 @@ public abstract class CoreEvent { public Guid Id { get; } = Guid.NewGuid(); public Guid? CorrelationId { get; init; } - public object Source { get; init; } + public object Source { get; set; } public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow; public DateTimeOffset? ProcessedAt { get; set; } } diff --git a/SharedLibraryCore/Events/Game/GameLogEvent.cs b/SharedLibraryCore/Events/Game/GameLogEvent.cs new file mode 100644 index 00000000..b6213307 --- /dev/null +++ b/SharedLibraryCore/Events/Game/GameLogEvent.cs @@ -0,0 +1,6 @@ +namespace SharedLibraryCore.Events.Game; + +public class GameLogEvent : GameEventV2 +{ + public string LogLine { get; set; } +} diff --git a/SharedLibraryCore/Events/Game/GameScriptEvent.cs b/SharedLibraryCore/Events/Game/GameScriptEvent.cs index c14f4bc7..b1f8af99 100644 --- a/SharedLibraryCore/Events/Game/GameScriptEvent.cs +++ b/SharedLibraryCore/Events/Game/GameScriptEvent.cs @@ -1,6 +1,77 @@ -namespace SharedLibraryCore.Events.Game; +using System; +using System.Reflection; +using SharedLibraryCore.Interfaces.Events; -public class GameScriptEvent : GameEventV2 +namespace SharedLibraryCore.Events.Game; + +public class GameScriptEvent : GameEventV2, IGameScriptEvent { - public string ScriptData { get; init; } + public string ScriptData { get; set; } + public string EventName { get; } = null; + + public virtual void ParseArguments() + { + var arguments = ScriptData.Split(';', StringSplitOptions.RemoveEmptyEntries); + + var propIndex = 0; + foreach (var argument in arguments) + { + var parts = argument.Split(['='], 2); + PropertyInfo propertyInfo = null; + string rawValue; + + if (parts.Length == 2) // handle as key/value pairs + { + var propertyName = parts[0].Trim(); + rawValue = parts[1].Trim(); + propertyInfo = GetType().GetProperty(propertyName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.DeclaredOnly); + } + else + { + rawValue = argument; + + try + { + propertyInfo = + GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase | + BindingFlags.DeclaredOnly)[ + propIndex]; + } + catch + { + // ignored + } + } + + if (propertyInfo is null) + { + continue; + } + + try + { + var method = propertyInfo.PropertyType.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, + [typeof(string)]); + + var convertedValue = method is not null + ? method!.Invoke(null, [rawValue])! + : Convert.ChangeType(rawValue, propertyInfo.PropertyType); + + propertyInfo.SetValue(this, convertedValue); + } + catch (TargetInvocationException ex) when (ex.InnerException is FormatException && + propertyInfo.PropertyType == typeof(bool)) + { + propertyInfo.SetValue(this, rawValue != "0"); + } + + catch + { + // ignored + } + + propIndex++; + } + } } diff --git a/SharedLibraryCore/Events/Management/ClientPenaltyEvent.cs b/SharedLibraryCore/Events/Management/ClientPenaltyEvent.cs index 54150e6b..3a5ee100 100644 --- a/SharedLibraryCore/Events/Management/ClientPenaltyEvent.cs +++ b/SharedLibraryCore/Events/Management/ClientPenaltyEvent.cs @@ -1,5 +1,5 @@ using Data.Models; -using Data.Models.Client; +using SharedLibraryCore.Database.Models; namespace SharedLibraryCore.Events.Management; diff --git a/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs b/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs new file mode 100644 index 00000000..977b38a1 --- /dev/null +++ b/SharedLibraryCore/Events/Server/ServerStatusReceiveEvent.cs @@ -0,0 +1,9 @@ +using SharedLibraryCore.Interfaces; + +namespace SharedLibraryCore.Events.Server; + +public class ServerStatusReceiveEvent : GameServerEvent +{ + public IStatusResponse Response { get; set; } + public string RawData { get; set; } +} diff --git a/SharedLibraryCore/Helpers/BuildNumberJsonConverter.cs b/SharedLibraryCore/Helpers/BuildNumberJsonConverter.cs index cdac5943..a5aae46a 100644 --- a/SharedLibraryCore/Helpers/BuildNumberJsonConverter.cs +++ b/SharedLibraryCore/Helpers/BuildNumberJsonConverter.cs @@ -1,27 +1,58 @@ using System; -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; -namespace SharedLibraryCore.Helpers +namespace SharedLibraryCore.Helpers; + +/// +/// JSON converter for the build number +/// +public class BuildNumberJsonConverter : JsonConverter { - /// - /// JSON converter for the build number - /// - public class BuildNumberJsonConverter : JsonConverter + public override BuildNumber Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(string); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, - JsonSerializer serializer) - { - return BuildNumber.Parse(reader.Value.ToString()); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(value.ToString()); - } + var stringValue = reader.GetString(); + return BuildNumber.Parse(stringValue); } -} \ No newline at end of file + + public override void Write(Utf8JsonWriter writer, BuildNumber value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} + +public class GameArrayJsonConverter : JsonConverter +{ + public override Server.Game[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + List games = []; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + var gameString = reader.GetString(); + var game = Enum.Parse(gameString); + games.Add(game); + } + + return games.ToArray(); + } + + public override void Write(Utf8JsonWriter writer, Server.Game[] value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (var game in value) + { + writer.WriteStringValue(game.ToString()); + } + + writer.WriteEndArray(); + } +} + diff --git a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs index b4807712..986728fb 100644 --- a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs +++ b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs @@ -86,6 +86,11 @@ public interface IGameEventSubscriptions /// static event Func ScriptEventTriggered; + /// + /// Raised when game log prints a line that is not handled by any other cases + /// + static event Func GameLogEventTriggered; + static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token) { return coreEvent switch @@ -100,6 +105,7 @@ public interface IGameEventSubscriptions ClientCommandEvent clientCommandEvent => ClientEnteredCommand?.InvokeAsync(clientCommandEvent, token) ?? Task.CompletedTask, ClientMessageEvent clientMessageEvent => ClientMessaged?.InvokeAsync(clientMessageEvent, token) ?? Task.CompletedTask, GameScriptEvent gameScriptEvent => ScriptEventTriggered?.InvokeAsync(gameScriptEvent, token) ?? Task.CompletedTask, + GameLogEvent gameLogEvent => GameLogEventTriggered?.InvokeAsync(gameLogEvent, token) ?? Task.CompletedTask, _ => Task.CompletedTask }; } @@ -116,5 +122,6 @@ public interface IGameEventSubscriptions ClientMessaged = null; ClientEnteredCommand = null; ScriptEventTriggered = null; + GameLogEventTriggered = null; } } diff --git a/SharedLibraryCore/Interfaces/Events/IGameScriptEvent.cs b/SharedLibraryCore/Interfaces/Events/IGameScriptEvent.cs new file mode 100644 index 00000000..b6e5458a --- /dev/null +++ b/SharedLibraryCore/Interfaces/Events/IGameScriptEvent.cs @@ -0,0 +1,8 @@ +namespace SharedLibraryCore.Interfaces.Events; + +public interface IGameScriptEvent +{ + string ScriptData { get; set; } + string EventName { get; } + void ParseArguments(); +} diff --git a/SharedLibraryCore/Interfaces/Events/IGameScriptEventFactory.cs b/SharedLibraryCore/Interfaces/Events/IGameScriptEventFactory.cs new file mode 100644 index 00000000..77bb71ce --- /dev/null +++ b/SharedLibraryCore/Interfaces/Events/IGameScriptEventFactory.cs @@ -0,0 +1,6 @@ +namespace SharedLibraryCore.Interfaces.Events; + +public interface IGameScriptEventFactory +{ + IGameScriptEvent Create(string eventType, string logData); +} diff --git a/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs index d2b72a33..3ceabea8 100644 --- a/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs +++ b/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs @@ -73,6 +73,11 @@ public interface IGameServerEventSubscriptions /// static event Func ServerValueSetCompleted; + /// + /// Raised when a server's status response is received + /// + static event Func ServerStatusReceived; + static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token) { return coreEvent switch @@ -88,6 +93,7 @@ public interface IGameServerEventSubscriptions ServerValueReceiveEvent serverValueReceiveEvent => ServerValueReceived?.InvokeAsync(serverValueReceiveEvent, token) ?? Task.CompletedTask, ServerValueSetRequestEvent serverValueSetRequestEvent => ServerValueSetRequested?.InvokeAsync(serverValueSetRequestEvent, token) ?? Task.CompletedTask, ServerValueSetCompleteEvent serverValueSetCompleteEvent => ServerValueSetCompleted?.InvokeAsync(serverValueSetCompleteEvent, token) ?? Task.CompletedTask, + ServerStatusReceiveEvent serverStatusReceiveEvent => ServerStatusReceived?.InvokeAsync(serverStatusReceiveEvent, token) ?? Task.CompletedTask, _ => Task.CompletedTask }; } diff --git a/SharedLibraryCore/Interfaces/IGameServer.cs b/SharedLibraryCore/Interfaces/IGameServer.cs index e28bd2b1..6ae70bdf 100644 --- a/SharedLibraryCore/Interfaces/IGameServer.cs +++ b/SharedLibraryCore/Interfaces/IGameServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Threading; using System.Threading.Tasks; using Data.Models; @@ -19,6 +20,9 @@ namespace SharedLibraryCore.Interfaces /// Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null); + IPEndPoint ResolvedIpEndPoint { get; } + IRConParser RconParser { get; } + /// /// Execute a server command /// @@ -35,72 +39,72 @@ namespace SharedLibraryCore.Interfaces /// /// Task SetDvarAsync(string name, object value, CancellationToken token = default); - + /// /// Time the most recent match ended /// DateTime? MatchEndTime { get; } - + /// /// Time the current match started /// DateTime? MatchStartTime { get; } - + /// /// List of connected clients /// IReadOnlyList ConnectedClients { get; } - + /// /// Game code corresponding to the development studio project /// Reference.Game GameCode { get; } - + /// /// Indicates if the anticheat/custom callbacks/live radar integration is enabled /// bool IsLegacyGameIntegrationEnabled { get; } - + /// /// Unique identifier for the server (typically ip:port) /// string Id { get; } - + /// /// Network address the server is listening on /// string ListenAddress { get; } - + /// /// Network port the server is listening on /// int ListenPort { get; } - + /// /// Name of the server (hostname) /// string ServerName { get; } - + /// /// Current gametype /// string Gametype { get; } - + /// /// Game password (required to join) /// string GamePassword { get; } - + /// /// Number of private client slots /// int PrivateClientSlots { get; } - + /// /// Current map the game server is running /// Map Map { get; } - + /// /// Database id for EFServer table and references /// diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 3f0b4c6e..8805f939 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -41,6 +41,7 @@ namespace SharedLibraryCore.Interfaces ILogger GetLogger(long serverId); IList GetServers(); + List Servers { get; } IList GetCommands(); IList GetMessageTokens(); IList GetActiveClients(); diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs index deee71d6..6316276d 100644 --- a/SharedLibraryCore/Interfaces/IRConParser.cs +++ b/SharedLibraryCore/Interfaces/IRConParser.cs @@ -15,35 +15,35 @@ namespace SharedLibraryCore.Interfaces /// /// stores the game/client specific version (usually the value of the "version" DVAR) /// - string Version { get; } + string Version { get; set; } /// /// specifies the game name (usually the internal studio iteration ie: IW4, T5 etc...) /// - Game GameName { get; } + Game GameName { get; set; } /// /// indicates if the game supports generating a log path from DVAR retrieval /// of fs_game, fs_basepath, g_log /// - bool CanGenerateLogPath { get; } + bool CanGenerateLogPath { get; set; } /// /// specifies the name of the parser /// - string Name { get; } + string Name { get; set; } /// /// specifies the type of rcon engine /// eg: COD, Source /// - string RConEngine { get; } + string RConEngine { get; set; } /// /// indicates that the game does not log to the mods folder (when mod is loaded), /// but rather always to the fs_basegame directory /// - bool IsOneLog { get; } + bool IsOneLog { get; set; } /// /// retrieves the value of a given DVAR @@ -54,7 +54,8 @@ namespace SharedLibraryCore.Interfaces /// default value to return if dvar retrieval fails /// /// - Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default); + Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default, + CancellationToken token = default); /// /// set value of DVAR by name @@ -65,7 +66,7 @@ namespace SharedLibraryCore.Interfaces /// /// Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default); - + /// /// executes a console command on the server /// diff --git a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs index f3da4b2c..c6473199 100644 --- a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs +++ b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs @@ -10,74 +10,74 @@ namespace SharedLibraryCore.Interfaces /// /// stores the command format for console commands /// - CommandPrefix CommandPrefixes { get; } + CommandPrefix CommandPrefixes { get; set; } /// /// stores the regex info for parsing get status response /// - ParserRegex Status { get; } + ParserRegex Status { get; set; } /// /// stores regex info for parsing the map line from rcon status response /// - ParserRegex MapStatus { get; } + ParserRegex MapStatus { get; set; } /// /// stores regex info for parsing the gametype line from rcon status response /// - ParserRegex GametypeStatus { get; } + ParserRegex GametypeStatus { get; set; } /// /// stores regex info for parsing hostname line from rcon status response /// - ParserRegex HostnameStatus { get; } + ParserRegex HostnameStatus { get; set; } /// /// stores regex info for parsing max players line from rcon status response /// - ParserRegex MaxPlayersStatus { get; } + ParserRegex MaxPlayersStatus { get; set; } /// /// stores the regex info for parsing get DVAR responses /// - ParserRegex Dvar { get; } + ParserRegex Dvar { get; set; } /// /// stores the regex info for parsing the header of a status response /// - ParserRegex StatusHeader { get; } + ParserRegex StatusHeader { get; set; } /// /// Specifies the expected response message from rcon when the server is not running /// - string ServerNotRunningResponse { get; } + string ServerNotRunningResponse { get; set; } /// /// indicates if the application should wait for response from server /// when executing a command /// - bool WaitForResponse { get; } + bool WaitForResponse { get; set; } /// /// indicates the format expected for parsed guids /// - NumberStyles GuidNumberStyle { get; } + NumberStyles GuidNumberStyle { get; set; } /// /// specifies simple mappings for dvar names in scenarios where the needed /// information is not stored in a traditional dvar name /// - IDictionary OverrideDvarNameMapping { get; } + IDictionary OverrideDvarNameMapping { get; set; } /// /// specifies the default dvar values for games that don't support certain dvars /// - IDictionary DefaultDvarValues { get; } + IDictionary DefaultDvarValues { get; set; } /// /// contains a setup of commands that have override timeouts /// - IDictionary OverrideCommandTimeouts { get; } + IDictionary OverrideCommandTimeouts { get; set; } /// /// specifies how many lines can be used for ingame notice @@ -87,29 +87,30 @@ namespace SharedLibraryCore.Interfaces /// /// specifies how many characters can be displayed per notice line /// - int NoticeMaxCharactersPerLine { get; } + int NoticeMaxCharactersPerLine { get; set; } /// /// specifies the characters used to split a line /// - string NoticeLineSeparator { get; } + string NoticeLineSeparator { get; set; } /// /// Default port the game listens to RCon requests on /// - int? DefaultRConPort { get; } + int? DefaultRConPort { get; set; } /// /// Default Indicator of where the game is installed (ex file path or registry entry) /// - string DefaultInstallationDirectoryHint { get; } + string DefaultInstallationDirectoryHint { get; set; } - ColorCodeMapping ColorCodeMapping { get; } + ColorCodeMapping ColorCodeMapping { get; set; } + + short FloodProtectInterval { get; set; } - short FloodProtectInterval { get; } /// /// indicates if diacritics (accented characters) should be normalized /// - bool ShouldRemoveDiacritics { get; } + bool ShouldRemoveDiacritics { get; set; } } } diff --git a/SharedLibraryCore/Interfaces/IStatusResponse.cs b/SharedLibraryCore/Interfaces/IStatusResponse.cs index 6f53b2ff..800b68b1 100644 --- a/SharedLibraryCore/Interfaces/IStatusResponse.cs +++ b/SharedLibraryCore/Interfaces/IStatusResponse.cs @@ -31,5 +31,10 @@ namespace SharedLibraryCore.Interfaces /// active clients /// EFClient[] Clients { get; } + + /// + /// raw text data from the game server + /// + string[] RawResponse { get; } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Localization/Layout.cs b/SharedLibraryCore/Localization/Layout.cs index a4b330e7..c7f58bb6 100644 --- a/SharedLibraryCore/Localization/Layout.cs +++ b/SharedLibraryCore/Localization/Layout.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Globalization; +using System.Text.Json.Serialization; using SharedLibraryCore.Interfaces; namespace SharedLibraryCore.Localization @@ -8,6 +9,8 @@ namespace SharedLibraryCore.Localization { private string localizationName; + public Layout() { } + public Layout(Dictionary set) { LocalizationIndex = new TranslationLookup @@ -27,7 +30,7 @@ namespace SharedLibraryCore.Localization } public TranslationLookup LocalizationIndex { get; set; } - public CultureInfo Culture { get; private set; } + [JsonIgnore] public CultureInfo Culture { get; private set; } } public class TranslationLookup : ITranslationLookup @@ -47,4 +50,4 @@ namespace SharedLibraryCore.Localization } } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index c5c764f4..64282eeb 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -120,6 +120,11 @@ namespace SharedLibraryCore.Database.Models [NotMapped] public string TimeSinceLastConnectionString => (DateTime.UtcNow - LastConnection).HumanizeForCurrentCulture(); + public DateTimeOffset LastCommandExecutionAttempt { get; set; } = DateTimeOffset.MinValue; + + [NotMapped] + public int CommandExecutionAttempts { get; set; } + [NotMapped] // this is kinda dirty, but I need localizable level names public ClientPermission ClientPermission => new ClientPermission diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 096794d4..44c7ade5 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -434,6 +434,7 @@ namespace SharedLibraryCore public abstract Task GetIdForServer(Server server = null); + [Obsolete("Use the ScriptPluginExtension helper")] public EFClient GetClientByNumber(int clientNumber) => GetClientsAsList().FirstOrDefault(client => client.ClientNumber == clientNumber); } diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 081458e6..9104cd43 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -2,9 +2,9 @@ Library - net6.0 + net8.0 RaidMax.IW4MAdmin.SharedLibraryCore - 2023.4.5.1 + 2024.01.01.1 RaidMax Forever None Debug;Release;Prerelease @@ -19,7 +19,7 @@ true MIT Shared Library for IW4MAdmin - 2023.4.5.1 + 2024.06.22.1 true $(NoWarn);1591 @@ -34,37 +34,31 @@ - + - - - - - - - - - - + + + + + + + + + - - true - Data.dll - + + true + Data.dll + - - - - - $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 6995f270..1ea1736a 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -868,7 +868,14 @@ namespace SharedLibraryCore { try { - return await server.RconParser.GetStatusAsync(server.RemoteConnection, token); + var response = await server.RconParser.GetStatusAsync(server.RemoteConnection, token); + + server.Manager.QueueEvent(new ServerStatusReceiveEvent + { + Response = response + }); + + return response; } catch (TaskCanceledException) @@ -1332,6 +1339,14 @@ namespace SharedLibraryCore return serviceCollection; } + + public static TimeSpan GetExponentialBackoffDelay(int retryCount, int staticDelay = 5) + { + var maxTimeout = TimeSpan.FromMinutes(2.1); + const double factor = 2.0; + var delay = Math.Min(staticDelay + Math.Pow(factor, retryCount - 1), maxTimeout.TotalSeconds); + return TimeSpan.FromSeconds(delay); + } public static void ExecuteAfterDelay(TimeSpan duration, Func action, CancellationToken token = default) => ExecuteAfterDelay((int)duration.TotalMilliseconds, action, token); diff --git a/WebfrontCore/Controllers/ConfigurationController.cs b/WebfrontCore/Controllers/ConfigurationController.cs index 784f3703..876c49e5 100644 --- a/WebfrontCore/Controllers/ConfigurationController.cs +++ b/WebfrontCore/Controllers/ConfigurationController.cs @@ -10,10 +10,9 @@ using SharedLibraryCore.Interfaces; using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using WebfrontCore.ViewModels; namespace WebfrontCore.Controllers @@ -92,15 +91,15 @@ namespace WebfrontCore.Controllers try { - var file = JObject.Parse(content); + var jsonDocument = JsonDocument.Parse(content); } - catch (JsonReaderException ex) + catch (JsonException ex) { return BadRequest($"{fileName}: {ex.Message}"); } - var path = System.IO.Path.Join(Utilities.OperatingDirectory, "Configuration", - fileName.Replace($"{System.IO.Path.DirectorySeparatorChar}", "")); + var path = Path.Join(Utilities.OperatingDirectory, "Configuration", + fileName.Replace($"{Path.DirectorySeparatorChar}", "")); // todo: move into a service at some point if (!System.IO.File.Exists(path)) diff --git a/WebfrontCore/Controllers/InteractionController.cs b/WebfrontCore/Controllers/InteractionController.cs index 8610219a..a006feae 100644 --- a/WebfrontCore/Controllers/InteractionController.cs +++ b/WebfrontCore/Controllers/InteractionController.cs @@ -26,6 +26,11 @@ public class InteractionController : BaseController return NotFound(); } + if (Client.Level < interactionData.MinimumPermission) + { + return Unauthorized(); + } + ViewBag.Title = interactionData.Description; var meta = HttpContext.Request.Query.ToDictionary(key => key.Key, value => value.Value.ToString()); var result = await _interactionRegistration.ProcessInteraction(interactionName, Client.ClientId, meta: meta, token: token); diff --git a/WebfrontCore/Properties/launchSettings.json b/WebfrontCore/Properties/launchSettings.json deleted file mode 100644 index a35479d6..00000000 --- a/WebfrontCore/Properties/launchSettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:61369/", - "sslPort": 0 - } - } -} \ No newline at end of file diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index b4c2361c..82e2cf25 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -15,7 +15,7 @@ - + diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 8ce9eb47..302b51bf 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 true true true @@ -33,22 +33,18 @@ - - - - + - - - - + + + @@ -78,7 +74,4 @@ - - - diff --git a/WebfrontCore/bundleconfig.json b/WebfrontCore/bundleconfig.json index bbce348f..5049558e 100644 --- a/WebfrontCore/bundleconfig.json +++ b/WebfrontCore/bundleconfig.json @@ -3,9 +3,9 @@ "outputFileName": "wwwroot/css/global.min.css", "inputFiles": [ "wwwroot/lib/halfmoon/css/halfmoon-variables.min.css", - "wwwroot/css/global.css", "wwwroot/lib/chart.js/dist/Chart.min.css", - "wwwroot/css/open-iconic.css" + "wwwroot/css/open-iconic-bootstrap-override.css", + "wwwroot/css/main.css" ] }, { diff --git a/WebfrontCore/compilerconfig.json b/WebfrontCore/compilerconfig.json deleted file mode 100644 index 63588138..00000000 --- a/WebfrontCore/compilerconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "outputFile": "wwwroot/css/global.css", - "inputFile": "wwwroot/css/src/main.scss" - }, - { - "outputFile": "wwwroot/css/src/profile.css", - "inputFile": "wwwroot/css/src/profile.scss" - }, - { - "outputFile": "wwwroot/css/open-iconic.css", - "inputFile": "wwwroot/lib/open-iconic/font/css/open-iconic-bootstrap-override.scss" - } -] diff --git a/WebfrontCore/compilerconfig.json.defaults b/WebfrontCore/compilerconfig.json.defaults deleted file mode 100644 index c75eb7d5..00000000 --- a/WebfrontCore/compilerconfig.json.defaults +++ /dev/null @@ -1,49 +0,0 @@ -{ - "compilers": { - "less": { - "autoPrefix": "", - "cssComb": "none", - "ieCompat": true, - "strictMath": false, - "strictUnits": false, - "relativeUrls": true, - "rootPath": "", - "sourceMapRoot": "", - "sourceMapBasePath": "", - "sourceMap": false - }, - "sass": { - "includePath": "", - "indentType": "space", - "indentWidth": 2, - "outputStyle": "nested", - "Precision": 5, - "relativeUrls": true, - "sourceMapRoot": "", - "sourceMap": false - }, - "stylus": { - "sourceMap": false - }, - "babel": { - "sourceMap": false - }, - "coffeescript": { - "bare": false, - "runtimeMode": "node", - "sourceMap": false - } - }, - "minifiers": { - "css": { - "enabled": true, - "termSemicolons": true, - "gzip": false - }, - "javascript": { - "enabled": true, - "termSemicolons": true, - "gzip": false - } - } -} \ No newline at end of file