1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-10 15:20:48 -05:00

Merge branch 'develop' into release/pre

This commit is contained in:
RaidMax
2024-06-30 12:09:13 -05:00
99 changed files with 1683 additions and 1242 deletions

257
.github/workflows/build_application.yml vendored Normal file
View File

@ -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

View File

@ -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

1
.gitignore vendored
View File

@ -247,3 +247,4 @@ launchSettings.json
*.db *.db
/Data/IW4MAdmin_Migration.db-shm /Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal /Data/IW4MAdmin_Migration.db-wal
bundle/

View File

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

View File

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

View File

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

View File

@ -1,31 +1,28 @@
using Newtonsoft.Json; using System.Text.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.API.Master namespace IW4MAdmin.Application.API.Master
{ {
public class ApiServer public class ApiServer
{ {
[JsonProperty("id")] [JsonPropertyName("id")]
public long Id { get; set; } public long Id { get; set; }
[JsonProperty("ip")] [JsonPropertyName("ip")]
public string IPAddress { get; set; } public string IPAddress { get; set; }
[JsonProperty("port")] [JsonPropertyName("port")]
public short Port { get; set; } public short Port { get; set; }
[JsonProperty("version")] [JsonPropertyName("version")]
public string Version { get; set; } public string Version { get; set; }
[JsonProperty("gametype")] [JsonPropertyName("gametype")]
public string Gametype { get; set; } public string Gametype { get; set; }
[JsonProperty("map")] [JsonPropertyName("map")]
public string Map { get; set; } public string Map { get; set; }
[JsonProperty("game")] [JsonPropertyName("game")]
public string Game { get; set; } public string Game { get; set; }
[JsonProperty("hostname")] [JsonPropertyName("hostname")]
public string Hostname { get; set; } public string Hostname { get; set; }
[JsonProperty("clientnum")] [JsonPropertyName("clientnum")]
public int ClientNum { get; set; } public int ClientNum { get; set; }
[JsonProperty("maxclientnum")] [JsonPropertyName("maxclientnum")]
public int MaxClientNum { get; set; } public int MaxClientNum { get; set; }
} }
} }

View File

@ -1,40 +1,37 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Application.Plugin; using IW4MAdmin.Application.Plugin;
using Newtonsoft.Json; using Refit;
using RestEase;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Application.API.Master namespace IW4MAdmin.Application.API.Master;
{
public class AuthenticationId public class AuthenticationId
{ {
[JsonProperty("id")] [JsonPropertyName("id")] public string Id { get; set; }
public string Id { get; set; }
} }
public class TokenId public class TokenId
{ {
[JsonProperty("access_token")] [JsonPropertyName("access_token")] public string AccessToken { get; set; }
public string AccessToken { get; set; }
} }
public class VersionInfo public class VersionInfo
{ {
[JsonProperty("current-version-stable")] [JsonPropertyName("current-version-stable")]
[JsonConverter(typeof(BuildNumberJsonConverter))] [JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber CurrentVersionStable { get; set; } public BuildNumber CurrentVersionStable { get; set; }
[JsonProperty("current-version-prerelease")] [JsonPropertyName("current-version-prerelease")]
[JsonConverter(typeof(BuildNumberJsonConverter))] [JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber CurrentVersionPrerelease { get; set; } public BuildNumber CurrentVersionPrerelease { get; set; }
} }
public class ResultMessage public class ResultMessage
{ {
[JsonProperty("message")] [JsonPropertyName("message")] public string Message { get; set; }
public string Message { get; set; }
} }
public class PluginSubscriptionContent public class PluginSubscriptionContent
@ -43,37 +40,31 @@ namespace IW4MAdmin.Application.API.Master
public PluginType Type { get; set; } public PluginType Type { get; set; }
} }
/// <summary> /// <summary>
/// Defines the capabilities of the master API /// Defines the capabilities of the master API
/// </summary> /// </summary>
[Header("User-Agent", "IW4MAdmin-RestEase")] [Headers("User-Agent: IW4MAdmin-RestEase")]
public interface IMasterApi public interface IMasterApi
{ {
[Header("Authorization")] [Post("/authenticate")]
string AuthorizationToken { get; set; }
[Post("authenticate")]
Task<TokenId> Authenticate([Body] AuthenticationId Id); Task<TokenId> Authenticate([Body] AuthenticationId Id);
[Post("instance/")] [Post("/instance/")]
[AllowAnyStatusCode] Task<IApiResponse<ResultMessage>> AddInstance([Body] ApiInstance instance, [Header("Authorization")] string authorization);
Task<Response<ResultMessage>> AddInstance([Body] ApiInstance instance);
[Put("instance/{id}")] [Put("/instance/{id}")]
[AllowAnyStatusCode] Task<IApiResponse<ResultMessage>> UpdateInstance(string id, [Body] ApiInstance instance, [Header("Authorization")] string authorization);
Task<Response<ResultMessage>> UpdateInstance([Path] string id, [Body] ApiInstance instance);
[Get("version/{apiVersion}")] [Get("/version/{apiVersion}")]
Task<VersionInfo> GetVersion([Path] int apiVersion); Task<VersionInfo> GetVersion(int apiVersion);
[Get("localization")] [Get("/localization")]
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization(); Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();
[Get("localization/{languageTag}")] [Get("/localization/{languageTag}")]
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag); Task<SharedLibraryCore.Localization.Layout> GetLocalization(string languageTag);
[Get("plugin_subscriptions")] [Get("/plugin_subscriptions")]
Task<IEnumerable<PluginSubscriptionContent>> GetPluginSubscription([Query("instance_id")] Guid instanceId, [Query("subscription_id")] string subscription_id); Task<IEnumerable<PluginSubscriptionContent>> GetPluginSubscription([Query] string instance_id,
} [Query] string subscription_id);
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId> <PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2020.0.0.0</Version> <Version>2020.0.0.0</Version>
@ -21,20 +21,21 @@
<Win32Resource /> <Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace> <RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest> <PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<Nullable>disable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2049" /> <PackageReference Include="Jint" Version="3.1.3" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" /> <PackageReference Include="MaxMind.GeoIP2" Version="5.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="RestEase" Version="1.5.7" /> <PackageReference Include="Refit" Version="7.1.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" /> <PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -72,18 +73,8 @@
</None> </None>
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(ConfigurationName)'=='Debug'">
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" /> <Exec Command="powershell.exe $(ProjectDir)BuildScripts\PreBuild.ps1 $(SolutionDir) $(TargetDir)" />
</Target> </Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %25(CurrentAssembly.Version)" />
</Target>
<Target Name="PostPublish" AfterTargets="Publish">
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
</Target>
</Project> </Project>

View File

@ -26,6 +26,7 @@ using Data.Abstractions;
using Data.Context; using Data.Context;
using Data.Models; using Data.Models;
using IW4MAdmin.Application.Configuration; using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script; using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -68,6 +69,7 @@ namespace IW4MAdmin.Application
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager; private readonly IAlertManager _alertManager;
private readonly ConfigurationWatcher _watcher;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
@ -94,7 +96,8 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins,
ConfigurationWatcher watcher)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
@ -102,10 +105,11 @@ namespace IW4MAdmin.Application
ClientSvc = clientService; ClientSvc = clientService;
PenaltySvc = penaltyService; PenaltySvc = penaltyService;
_alertManager = alertManager; _alertManager = alertManager;
_watcher = watcher;
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) }; AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig, serviceProvider.GetRequiredService<IGameScriptEventFactory>()) };
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
@ -529,6 +533,7 @@ namespace IW4MAdmin.Application
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]); Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
_watcher.Enable();
IsInitialized = true; IsInitialized = true;
} }
@ -710,7 +715,7 @@ namespace IW4MAdmin.Application
public IEventParser GenerateDynamicEventParser(string name) public IEventParser GenerateDynamicEventParser(string name)
{ {
return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration()) return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration(), _serviceProvider.GetRequiredService<IGameScriptEventFactory>())
{ {
Name = name Name = name
}; };

View File

@ -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
}

View File

@ -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*"

View File

@ -0,0 +1,50 @@
#!/bin/bash
PublishDir="$1"
SourceDir="$2"
if [ -z "$PublishDir" ] || [ -z "$SourceDir" ]; then
echo "Usage: $0 <PublishDir> <SourceDir>"
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

View File

@ -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

View File

@ -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%

View File

@ -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/"

View File

@ -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/"

View File

@ -8,6 +8,7 @@ using System.Linq;
using Data.Models; using Data.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Game; using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Interfaces.Events;
using static System.Int32; using static System.Int32;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -21,16 +22,18 @@ namespace IW4MAdmin.Application.EventParsers
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly IGameScriptEventFactory _gameScriptEventFactory;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap; private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap; private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger,
ApplicationConfiguration appConfig) ApplicationConfiguration appConfig, IGameScriptEventFactory gameScriptEventFactory)
{ {
_customEventRegistrations = _customEventRegistrations =
new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>(); new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger; _logger = logger;
_appConfig = appConfig; _appConfig = appConfig;
_gameScriptEventFactory = gameScriptEventFactory;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory) Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{ {
@ -183,14 +186,28 @@ namespace IW4MAdmin.Application.EventParsers
if (logLine.StartsWith("GSE;")) if (logLine.StartsWith("GSE;"))
{ {
return new GameScriptEvent var gscEvent = new GameScriptEvent
{ {
ScriptData = logLine, ScriptData = logLine,
GameTime = gameTime, GameTime = gameTime,
Source = GameEvent.EventSource.Log 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)) if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey))
{ {
return GenerateDefaultEvent(logLine, gameTime); return GenerateDefaultEvent(logLine, gameTime);
@ -218,12 +235,13 @@ namespace IW4MAdmin.Application.EventParsers
return GenerateDefaultEvent(logLine, gameTime); 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, Type = GameEvent.EventType.Unknown,
Data = logLine, Data = logLine,
LogLine = logLine,
Origin = Utilities.IW4MAdminClient(), Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,

View File

@ -1,5 +1,6 @@
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
@ -10,7 +11,9 @@ namespace IW4MAdmin.Application.EventParsers
/// </summary> /// </summary>
sealed internal class DynamicEventParser : BaseEventParser 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)
{ {
} }
} }

View File

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Data.Models.Client;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Extensions; 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(); 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());
} }

View File

@ -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<string, Type> _gameScriptEventMap;
public GameScriptEventFactory(IServiceProvider serviceProvider, IEnumerable<IGameScriptEvent> 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;
}
}

View File

@ -71,7 +71,6 @@ public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHand
await using var fileStream = File.OpenRead(_path); await using var fileStream = File.OpenRead(_path);
readConfiguration = readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions); await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
_watcher.Register(_path, FileUpdated); _watcher.Register(_path, FileUpdated);
if (readConfiguration is null) if (readConfiguration is null)
@ -131,7 +130,6 @@ public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHand
await using var fileStream = File.Create(_path); await using var fileStream = File.Create(_path);
await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions); await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions);
await fileStream.DisposeAsync();
_configurationInstance = configuration; _configurationInstance = configuration;
} }
catch (Exception ex) catch (Exception ex)
@ -155,7 +153,6 @@ public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHand
await using var fileStream = File.OpenRead(_path); await using var fileStream = File.OpenRead(_path);
var readConfiguration = var readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions); await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
if (readConfiguration is null) if (readConfiguration is null)
{ {

View File

@ -20,7 +20,6 @@ public sealed class ConfigurationWatcher : IDisposable
}; };
_watcher.Changed += WatcherOnChanged; _watcher.Changed += WatcherOnChanged;
_watcher.EnableRaisingEvents = true;
} }
public void Dispose() public void Dispose()
@ -31,30 +30,28 @@ public sealed class ConfigurationWatcher : IDisposable
public void Register(string fileName, Action<string> fileUpdated) public void Register(string fileName, Action<string> fileUpdated)
{ {
if (_registeredActions.ContainsKey(fileName)) _registeredActions.TryAdd(fileName, fileUpdated);
{
return;
}
_registeredActions.Add(fileName, fileUpdated);
} }
public void Unregister(string fileName) 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) 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) new FileInfo(eventArgs.FullPath).Length == 0)
{ {
return; return;
} }
_registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath); value.Invoke(eventArgs.FullPath);
} }
} }

View File

@ -82,7 +82,7 @@ namespace IW4MAdmin.Application.IO
{ {
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != Utilities.WORLD_ID) 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) if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)

View File

@ -1,5 +1,4 @@
using IW4MAdmin.Application.API.GameLogServer; using IW4MAdmin.Application.API.GameLogServer;
using RestEase;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
@ -7,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Refit;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.IO
public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger<GameLogReaderHttp> logger) public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger<GameLogReaderHttp> logger)
{ {
_eventParser = parser; _eventParser = parser;
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString()); _logServerApi = RestService.For<IGameLogServer>(gameLogServerUris[0].ToString());
_safeLogPath = gameLogServerUris[1].LocalPath.ToBase64UrlSafeString(); _safeLogPath = gameLogServerUris[1].LocalPath.ToBase64UrlSafeString();
_logger = logger; _logger = logger;
} }

View File

@ -25,6 +25,7 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models; using Data.Models;
using Data.Models.Server; using Data.Models.Server;
using Humanizer;
using IW4MAdmin.Application.Alerts; using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands; using IW4MAdmin.Application.Commands;
using IW4MAdmin.Application.Plugin.Script; using IW4MAdmin.Application.Plugin.Script;
@ -193,11 +194,47 @@ namespace IW4MAdmin
Command command = null; Command command = null;
if (E.Type == GameEvent.EventType.Command) 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 try
{ {
command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration); command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
} }
catch (CommandException e) catch (CommandException e)
{ {
ServerLogger.LogWarning(e, "Error validating command from event {@Event}", ServerLogger.LogWarning(e, "Error validating command from event {@Event}",

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -59,11 +60,11 @@ namespace IW4MAdmin.Application.Localization
var localizationDict = new Dictionary<string, string>(); var localizationDict = new Dictionary<string, string>();
foreach (string filePath in localizationFiles) foreach (var filePath in localizationFiles)
{ {
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8); var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents); var eachLocalizationFile = JsonSerializer.Deserialize<SharedLibraryCore.Localization.Layout>(localizationContents);
if (eachLocalizationFile == null) if (eachLocalizationFile is null)
{ {
continue; continue;
} }
@ -72,7 +73,7 @@ namespace IW4MAdmin.Application.Localization
{ {
if (!localizationDict.TryAdd(item.Key, item.Value)) 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);
} }
} }
} }

View File

@ -5,7 +5,6 @@ using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using RestEase;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; 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.Abstractions;
using IW4MAdmin.Plugins.Stats.Client; using IW4MAdmin.Plugins.Stats.Client;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Refit;
using SharedLibraryCore.Interfaces.Events;
using Stats.Client.Abstractions; using Stats.Client.Abstractions;
using Stats.Client; using Stats.Client;
using Stats.Config; using Stats.Config;
@ -94,15 +95,6 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}"); Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("====================================================="); 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(); await LaunchAsync();
} }
@ -451,12 +443,12 @@ namespace IW4MAdmin.Application
var masterUri = Utilities.IsDevelopment var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080") ? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl; : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var httpClient = new HttpClient var httpClient = new HttpClient(new HttpClientHandler {AllowAutoRedirect = true})
{ {
BaseAddress = masterUri, BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15) Timeout = TimeSpan.FromSeconds(15)
}; };
var masterRestClient = RestClient.For<IMasterApi>(httpClient); var masterRestClient = RestService.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig); var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null) if (appConfig == null)
@ -469,10 +461,7 @@ namespace IW4MAdmin.Application
// register override level names // register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames) foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{ {
if (!Utilities.PermissionLevelOverrides.ContainsKey(key)) Utilities.PermissionLevelOverrides.TryAdd(key, value);
{
Utilities.PermissionLevelOverrides.Add(key, value);
}
} }
// build the dependency list // build the dependency list
@ -539,6 +528,7 @@ namespace IW4MAdmin.Application
.AddSingleton(new ConfigurationWatcher()) .AddSingleton(new ConfigurationWatcher())
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>)) .AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>() .AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton<IGameScriptEventFactory, GameScriptEventFactory>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);

View File

@ -1,5 +1,4 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.API.Master;
using RestEase;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
@ -9,6 +8,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Refit;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
@ -28,6 +28,7 @@ namespace IW4MAdmin.Application.Misc
private readonly int _apiVersion = 1; private readonly int _apiVersion = 1;
private bool _firstHeartBeat = true; private bool _firstHeartBeat = true;
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30); private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
private string _authorizationToken;
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager) public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{ {
@ -128,7 +129,7 @@ namespace IW4MAdmin.Application.Misc
Id = _appConfig.Id Id = _appConfig.Id
}); });
_apiInstance.AuthorizationToken = $"Bearer {token.AccessToken}"; _authorizationToken = $"Bearer {token.AccessToken}";
} }
var instance = new ApiInstance var instance = new ApiInstance
@ -153,22 +154,22 @@ namespace IW4MAdmin.Application.Misc
WebfrontUrl = _appConfig.WebfrontUrl WebfrontUrl = _appConfig.WebfrontUrl
}; };
Response<ResultMessage> response; IApiResponse<ResultMessage> response;
if (_firstHeartBeat) if (_firstHeartBeat)
{ {
response = await _apiInstance.AddInstance(instance); response = await _apiInstance.AddInstance(instance, _authorizationToken);
} }
else else
{ {
response = await _apiInstance.UpdateInstance(instance.Id, instance); response = await _apiInstance.UpdateInstance(instance.Id, instance, _authorizationToken);
_firstHeartBeat = false; _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);
} }
} }
} }

View File

@ -55,7 +55,7 @@ namespace IW4MAdmin.Application.Misc
var decryptedContent = new byte[encryptedContent.Length]; var decryptedContent = new byte[encryptedContent.Length];
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512); 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 try
{ {

View File

@ -1,150 +1,255 @@
using Newtonsoft.Json; using SharedLibraryCore;
using Newtonsoft.Json.Linq;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using System; using System;
using System.Net; using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Data.Models; using Data.Models;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc;
public class IPAddressConverter : JsonConverter<IPAddress>
{ {
class IPAddressConverter : JsonConverter public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
public override bool CanConvert(Type objectType) var ipAddressString = reader.GetString();
{ return IPAddress.Parse(ipAddressString);
return (objectType == typeof(IPAddress));
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options)
{ {
writer.WriteValue(value.ToString()); writer.WriteStringValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return IPAddress.Parse((string)reader.Value);
} }
} }
class IPEndPointConverter : JsonConverter public class IPEndPointConverter : JsonConverter<IPEndPoint>
{ {
public override bool CanConvert(Type objectType) public override IPEndPoint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
return (objectType == typeof(IPEndPoint)); 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;
}
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) if (reader.TokenType == JsonTokenType.EndObject)
{ {
IPEndPoint ep = (IPEndPoint)value; break;
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<IPAddress>(serializer);
int port = (int)jo["Port"];
return new IPEndPoint(address, port); 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();
}
} }
class ClientEntityConverter : JsonConverter public class ClientEntityConverter : JsonConverter<EFClient>
{ {
public override bool CanConvert(Type objectType) => objectType == typeof(EFClient); public override EFClient Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override object ReadJson(JsonReader reader, Type objectType,object existingValue, JsonSerializer serializer)
{ {
if (reader.Value == null) if (reader.TokenType == JsonTokenType.Null)
{ {
return null; return null;
} }
var jsonObject = JObject.Load(reader); 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 return new EFClient
{ {
NetworkId = (long)jsonObject["NetworkId"], NetworkId = networkId,
ClientNumber = (int)jsonObject["ClientNumber"], ClientNumber = clientNumber,
State = Enum.Parse<ClientState>(jsonObject["state"].ToString()), State = state,
CurrentAlias = new EFAlias() CurrentAlias = currentAlias
{
IPAddress = (int?)jsonObject["IPAddress"],
Name = jsonObject["Name"].ToString()
}
}; };
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void Write(Utf8JsonWriter writer, EFClient value, JsonSerializerOptions options)
{ {
var client = value as EFClient; writer.WriteStartObject();
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); 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();
} }
} }
class GameEventConverter : JsonConverter public class GameEventConverter : JsonConverter<GameEvent>
{ {
public override bool CanConvert(Type objectType) =>objectType == typeof(GameEvent); public override GameEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{ {
var jsonObject = JObject.Load(reader); if (reader.TokenType == JsonTokenType.Null)
return new GameEvent
{ {
Type = Enum.Parse<EventType>(jsonObject["Type"].ToString()), return null;
Subtype = jsonObject["Subtype"]?.ToString(),
Source = Enum.Parse<EventSource>(jsonObject["Source"].ToString()),
RequiredEntity = Enum.Parse<EventRequiredEntity>(jsonObject["RequiredEntity"].ToString()),
Data = jsonObject["Data"].ToString(),
Message = jsonObject["Message"].ToString(),
GameTime = (int?)jsonObject["GameTime"],
Origin = jsonObject["Origin"]?.ToObject<EFClient>(serializer),
Target = jsonObject["Target"]?.ToObject<EFClient>(serializer),
ImpersonationOrigin = jsonObject["ImpersonationOrigin"]?.ToObject<EFClient>(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 = new GameEvent();
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{ {
var gameEvent = value as GameEvent; if (reader.TokenType == JsonTokenType.PropertyName)
var jsonObject = new JObject
{ {
{ "Type", (int)gameEvent.Type }, var propertyName = reader.GetString();
{ "Subtype", gameEvent.Subtype }, reader.Read();
{ "Source", (int)gameEvent.Source }, switch (propertyName)
{ "RequiredEntity", (int)gameEvent.RequiredEntity }, {
{ "Data", gameEvent.Data }, case "Type":
{ "Message", gameEvent.Message }, gameEvent.Type = (EventType)reader.GetInt32();
{ "GameTime", gameEvent.GameTime }, break;
{ "Origin", gameEvent.Origin != null ? JToken.FromObject(gameEvent.Origin, serializer) : null }, case "Subtype":
{ "Target", gameEvent.Target != null ? JToken.FromObject(gameEvent.Target, serializer) : null }, gameEvent.Subtype = reader.GetString();
{ "ImpersonationOrigin", gameEvent.ImpersonationOrigin != null ? JToken.FromObject(gameEvent.ImpersonationOrigin, serializer) : null}, break;
{ "IsRemote", gameEvent.IsRemote }, case "Source":
{ "Extra", gameEvent.Extra?.ToString() }, gameEvent.Source = (EventSource)reader.GetInt32();
{ "Time", gameEvent.Time }, break;
{ "IsBlocking", gameEvent.IsBlocking } case "RequiredEntity":
}; gameEvent.RequiredEntity = (EventRequiredEntity)reader.GetInt32();
break;
jsonObject.WriteTo(writer); 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<EFClient>(ref reader, options);
break;
case "Target":
gameEvent.Target = JsonSerializer.Deserialize<EFClient>(ref reader, options);
break;
case "ImpersonationOrigin":
gameEvent.ImpersonationOrigin = JsonSerializer.Deserialize<EFClient>(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();
}
}

View File

@ -167,7 +167,7 @@ namespace IW4MAdmin.Application.Plugin
try try
{ {
_pluginSubscription ??= _masterApi _pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result; .GetPluginSubscription(_appConfig.Id, _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription
.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray()); .Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
@ -185,7 +185,7 @@ namespace IW4MAdmin.Application.Plugin
try try
{ {
_pluginSubscription ??= _masterApi _pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result; .GetPluginSubscription(_appConfig.Id, _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription
.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray()); .Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());

View File

@ -23,6 +23,7 @@ using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using Reference = Data.Models.Reference;
namespace IW4MAdmin.Application.Plugin.Script namespace IW4MAdmin.Application.Plugin.Script
{ {

View File

@ -26,6 +26,7 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events; using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using JavascriptEngine = Jint.Engine; using JavascriptEngine = Jint.Engine;
using Reference = Data.Models.Reference;
namespace IW4MAdmin.Application.Plugin.Script; namespace IW4MAdmin.Application.Plugin.Script;

View File

@ -91,9 +91,9 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection) ? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection)
: iqGroupedClientAliases.OrderBy(clientAlias => 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) .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 // this pulls in more records than we need, but it's more efficient than ordering grouped entities
var clientLookups = await clientAliases var clientLookups = await clientAliases

View File

@ -175,6 +175,7 @@ namespace IW4MAdmin.Application.RConParsers
return new StatusResponse return new StatusResponse
{ {
RawResponse = response,
Clients = ClientsFromStatus(response).ToArray(), Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus), Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus), GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus),

View File

@ -6,10 +6,11 @@ namespace IW4MAdmin.Application.RConParsers
/// <inheritdoc cref="IStatusResponse"/> /// <inheritdoc cref="IStatusResponse"/>
public class StatusResponse : IStatusResponse public class StatusResponse : IStatusResponse
{ {
public string Map { get; set; } public string Map { get; init; }
public string GameType { get; set; } public string GameType { get; init; }
public string Hostname { get; set; } public string Hostname { get; init; }
public int? MaxClients { get; set; } public int? MaxClients { get; init; }
public EFClient[] Clients { get; set; } public EFClient[] Clients { get; init; }
public string[] RawResponse { get; set; }
} }
} }

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms> <Platforms>AnyCPU</Platforms>
<Title>RaidMax.IW4MAdmin.Data</Title> <Title>RaidMax.IW4MAdmin.Data</Title>
@ -9,16 +9,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1"> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql" Version="6.0.2" /> <PackageReference Include="Npgsql" Version="8.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,12 +1,13 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
// ReSharper disable CompareOfFloatsByEqualityOperator // ReSharper disable CompareOfFloatsByEqualityOperator
#pragma warning disable CS0659 #pragma warning disable CS0659
namespace Data.Models namespace Data.Models
{ {
public class Vector3 public class Vector3 : IParsable<Vector3>
{ {
[Key] public int Vector3Id { get; set; } [Key] public int Vector3Id { get; set; }
public float X { get; protected set; } public float X { get; protected set; }
@ -111,5 +112,30 @@ namespace Data.Models
public double Magnitude() => Math.Sqrt((X * X) + (Y * Y) + (Z * Z)); 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 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;
}
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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'

View File

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

View File

@ -6,13 +6,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml
DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1
README.md = README.md README.md = README.md
version.txt = version.txt version.txt = version.txt
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1 DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
DeploymentFiles\nuget-pipeline.yml = DeploymentFiles\nuget-pipeline.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"
@ -112,6 +109,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto IW5", "Pluto IW5", "{
GameFiles\AntiCheat\IW5\README.MD = GameFiles\AntiCheat\IW5\README.MD GameFiles\AntiCheat\IW5\README.MD = GameFiles\AntiCheat\IW5\README.MD
EndProjectSection EndProjectSection
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -461,6 +464,7 @@ Global
{3EA564BD-3AC6-479B-96B6-CB059DCD0C77} = {AB83BAC0-C539-424A-BF00-78487C10753C} {3EA564BD-3AC6-479B-96B6-CB059DCD0C77} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{866F453D-BC89-457F-8B55-485494759B31} = {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} {603725A4-BC0B-423B-955B-762C89E1C4C2} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{DCCEED9F-816E-4595-8B74-D76A77FBE0BE} = {8C8F3945-0AEF-4949-A1F7-B18E952E50BC}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<AssemblyName>Integrations.Cod</AssemblyName> <AssemblyName>Integrations.Cod</AssemblyName>
<RootNamespace>Integrations.Cod</RootNamespace> <RootNamespace>Integrations.Cod</RootNamespace>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -17,7 +17,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="protobuf-net" Version="3.2.26" /> <PackageReference Include="protobuf-net" Version="3.2.30" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<AssemblyName>Integrations.Source</AssemblyName> <AssemblyName>Integrations.Source</AssemblyName>
<RootNamespace>Integrations.Source</RootNamespace> <RootNamespace>Integrations.Source</RootNamespace>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch> <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -10,11 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project> </Project>

View File

@ -3,7 +3,6 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using IW4MAdmin.Plugins.LiveRadar.Configuration; using IW4MAdmin.Plugins.LiveRadar.Configuration;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -82,7 +81,7 @@ namespace IW4MAdmin.Plugins.LiveRadar.Web.Controllers
} }
var radarInfo = server.GetClientsAsList() var radarInfo = server.GetClientsAsList()
.Select(client => client.GetAdditionalProperty<RadarEvent>("LiveRadar")).ToList(); .Select(client => client.GetAdditionalProperty<RadarDto>("LiveRadar")).ToList();
return Json(radarInfo); return Json(radarInfo);
} }

View File

@ -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<string, string,
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
SharedLibraryCore.GameEvent,
SharedLibraryCore.GameEvent>>;
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<EventGeneratorCallback> Events => new[] { LiveRadar() };
}
public class LiveRadarEvent : GameScriptEvent
{
}

View File

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

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild> <RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish> <RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
<PreserveCompilationContext Condition="'$(CONFIG)'!='Debug'">false</PreserveCompilationContext> <PreserveCompilationContext>false</PreserveCompilationContext>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc> <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Version>0.1.0.0</Version> <Version>0.1.0.0</Version>
@ -16,11 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project> </Project>

View File

@ -36,6 +36,9 @@ public class Plugin : IPluginV2
public static void RegisterDependencies(IServiceCollection serviceCollection) public static void RegisterDependencies(IServiceCollection serviceCollection)
{ {
serviceCollection.AddConfiguration<LiveRadarConfiguration>(); serviceCollection.AddConfiguration<LiveRadarConfiguration>();
serviceCollection.AddSingleton<IGameScriptEvent, LiveRadarScriptEvent>(); // for identification
serviceCollection.AddTransient<LiveRadarScriptEvent>(); // for factory
} }
public Plugin(ILogger<Plugin> logger, ApplicationConfiguration appConfig) public Plugin(ILogger<Plugin> logger, ApplicationConfiguration appConfig)
@ -51,7 +54,7 @@ public class Plugin : IPluginV2
private Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token) private Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token)
{ {
if (scriptEvent is not LiveRadarEvent radarEvent) if (scriptEvent is not LiveRadarScriptEvent radarEvent)
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -83,14 +86,15 @@ public class Plugin : IPluginV2
: (originalBotGuid ?? "0").ConvertGuidToLong(NumberStyles.HexNumber); : (originalBotGuid ?? "0").ConvertGuidToLong(NumberStyles.HexNumber);
} }
var radarUpdate = RadarEvent.Parse(scriptEvent.ScriptData, generatedBotGuid); var radarDto = RadarDto.FromScriptEvent(radarEvent, generatedBotGuid);
var client = var client =
radarEvent.Owner.ConnectedClients.FirstOrDefault(client => client.NetworkId == radarUpdate.Guid); radarEvent.Owner.ConnectedClients.FirstOrDefault(client => client.NetworkId == radarDto.Guid);
if (client != null) if (client != null)
{ {
radarUpdate.Name = client.Name.StripColors(); radarDto.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate); client.SetAdditionalProperty("LiveRadar", radarDto);
} }
} }

View File

@ -1,13 +1,13 @@
using Data.Models; using Data.Models;
using SharedLibraryCore; using SharedLibraryCore;
using System; using IW4MAdmin.Plugins.LiveRadar.Events;
using System.Linq;
// ReSharper disable CompareOfFloatsByEqualityOperator // ReSharper disable CompareOfFloatsByEqualityOperator
#pragma warning disable CS0659 #pragma warning disable CS0659
namespace IW4MAdmin.Plugins.LiveRadar; namespace IW4MAdmin.Plugins.LiveRadar;
public class RadarEvent public class RadarDto
{ {
public string Name { get; set; } public string Name { get; set; }
public long Guid { get; set; } public long Guid { get; set; }
@ -26,7 +26,7 @@ public class RadarEvent
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
if (obj is RadarEvent re) if (obj is RadarDto re)
{ {
return re.ViewAngles.X == ViewAngles.X && return re.ViewAngles.X == ViewAngles.X &&
re.ViewAngles.Y == ViewAngles.Y && re.ViewAngles.Y == ViewAngles.Y &&
@ -39,23 +39,21 @@ public class RadarEvent
return false; 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 RadarDto
var parsedEvent = new RadarEvent()
{ {
Guid = generatedBotGuid, Guid = generatedBotGuid,
Location = Vector3.Parse(items[1]), Location = scriptEvent.Location,
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(), ViewAngles = scriptEvent.ViewAngles.FixIW4Angles(),
Team = items[3], Team = scriptEvent.Team,
Kills = int.Parse(items[4]), Kills = scriptEvent.Kills,
Deaths = int.Parse(items[5]), Deaths = scriptEvent.Deaths,
Score = int.Parse(items[6]), Score = scriptEvent.Score,
Weapon = items[7], Weapon =scriptEvent.Weapon,
Health = int.Parse(items[8]), Health = scriptEvent.Health,
IsAlive = items[9] == "1", IsAlive = scriptEvent.IsAlive,
PlayTime = Convert.ToInt32(items[10]) PlayTime = scriptEvent.PlayTime
}; };
return parsedEvent; return parsedEvent;

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ApplicationIcon /> <ApplicationIcon />
<StartupObject /> <StartupObject />
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
@ -19,11 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project> </Project>

View File

@ -20,8 +20,8 @@ public class MuteCommand : Command
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
SupportedGames = Plugin.SupportedGames; SupportedGames = Plugin.SupportedGames;
Arguments = new[] Arguments =
{ [
new CommandArgument new CommandArgument
{ {
Name = translationLookup["COMMANDS_ARGS_PLAYER"], Name = translationLookup["COMMANDS_ARGS_PLAYER"],
@ -32,7 +32,7 @@ public class MuteCommand : Command
Name = translationLookup["COMMANDS_ARGS_REASON"], Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true Required = true
} }
}; ];
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)

View File

@ -21,14 +21,14 @@ public class MuteInfoCommand : Command
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
SupportedGames = Plugin.SupportedGames; SupportedGames = Plugin.SupportedGames;
Arguments = new[] Arguments =
{ [
new CommandArgument new CommandArgument
{ {
Name = translationLookup["COMMANDS_ARGS_PLAYER"], Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true Required = true
} }
}; ];
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)

View File

@ -7,10 +7,9 @@ using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Plugins.Mute.Commands; namespace IW4MAdmin.Plugins.Mute.Commands;
public class TempMuteCommand : Command public partial class TempMuteCommand : Command
{ {
private readonly MuteManager _muteManager; private readonly MuteManager _muteManager;
private const string TempBanRegex = @"([0-9]+\w+)\ (.+)";
public TempMuteCommand(CommandConfiguration config, ITranslationLookup translationLookup, MuteManager muteManager) : base(config, public TempMuteCommand(CommandConfiguration config, ITranslationLookup translationLookup, MuteManager muteManager) : base(config,
translationLookup) translationLookup)
@ -22,8 +21,8 @@ public class TempMuteCommand : Command
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
SupportedGames = Plugin.SupportedGames; SupportedGames = Plugin.SupportedGames;
Arguments = new[] Arguments =
{ [
new CommandArgument new CommandArgument
{ {
Name = translationLookup["COMMANDS_ARGS_PLAYER"], Name = translationLookup["COMMANDS_ARGS_PLAYER"],
@ -39,7 +38,7 @@ public class TempMuteCommand : Command
Name = translationLookup["COMMANDS_ARGS_REASON"], Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true Required = true
} }
}; ];
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
@ -50,7 +49,7 @@ public class TempMuteCommand : Command
return; return;
} }
var match = Regex.Match(gameEvent.Data, TempBanRegex); var match = TempBanRegex().Match(gameEvent.Data);
if (match.Success) if (match.Success)
{ {
var expiration = DateTime.UtcNow + match.Groups[1].ToString().ParseTimespan(); 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"]); gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_BAD_FORMAT"]);
} }
[GeneratedRegex(@"([0-9]+\w+)\ (.+)")]
private static partial Regex TempBanRegex();
} }

View File

@ -20,8 +20,8 @@ public class UnmuteCommand : Command
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
SupportedGames = Plugin.SupportedGames; SupportedGames = Plugin.SupportedGames;
Arguments = new[] Arguments =
{ [
new CommandArgument new CommandArgument
{ {
Name = translationLookup["COMMANDS_ARGS_PLAYER"], Name = translationLookup["COMMANDS_ARGS_PLAYER"],
@ -32,7 +32,7 @@ public class UnmuteCommand : Command
Name = translationLookup["COMMANDS_ARGS_REASON"], Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true Required = true
} }
}; ];
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Authors>MrAmos123</Authors> <Authors>MrAmos123</Authors>
@ -12,10 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project> </Project>

View File

@ -10,23 +10,15 @@ using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Plugins.Mute; namespace IW4MAdmin.Plugins.Mute;
public class MuteManager public class MuteManager(
ILogger<MuteManager> logger,
IDatabaseContextFactory databaseContextFactory,
IMetaServiceV2 metaService,
ITranslationLookup translationLookup)
{ {
private readonly IMetaServiceV2 _metaService; private readonly ILogger _logger = logger;
private readonly ITranslationLookup _translationLookup;
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _databaseContextFactory;
private readonly SemaphoreSlim _onMuteAction = new(1, 1); private readonly SemaphoreSlim _onMuteAction = new(1, 1);
public MuteManager(ILogger<MuteManager> logger, IDatabaseContextFactory databaseContextFactory,
IMetaServiceV2 metaService, ITranslationLookup translationLookup)
{
_logger = logger;
_databaseContextFactory = databaseContextFactory;
_metaService = metaService;
_translationLookup = translationLookup;
}
public static bool IsExpiredMute(MuteStateMeta muteStateMeta) => public static bool IsExpiredMute(MuteStateMeta muteStateMeta) =>
muteStateMeta.Expiration is not null && muteStateMeta.Expiration < DateTime.UtcNow; muteStateMeta.Expiration is not null && muteStateMeta.Expiration < DateTime.UtcNow;
@ -42,7 +34,7 @@ public class MuteManager
var muteState = await ReadPersistentDataV1(client); var muteState = await ReadPersistentDataV1(client);
clientMuteMeta = new MuteStateMeta 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 Expiration = muteState switch
{ {
null => DateTime.UtcNow, null => DateTime.UtcNow,
@ -149,7 +141,7 @@ public class MuteManager
private async Task ExpireMutePenalties(EFClient client) private async Task ExpireMutePenalties(EFClient client)
{ {
await using var context = _databaseContextFactory.CreateContext(); await using var context = databaseContextFactory.CreateContext();
var mutePenalties = await context.Penalties var mutePenalties = await context.Penalties
.Where(penalty => penalty.OffenderId == client.ClientId) .Where(penalty => penalty.OffenderId == client.ClientId)
.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Mute || penalty.Type == EFPenalty.PenaltyType.TempMute) .Where(penalty => penalty.Type == EFPenalty.PenaltyType.Mute || penalty.Type == EFPenalty.PenaltyType.TempMute)
@ -184,7 +176,7 @@ public class MuteManager
} }
private async Task<MuteState?> ReadPersistentDataV1(EFClient client) => TryParse<MuteState>( private async Task<MuteState?> ReadPersistentDataV1(EFClient client) => TryParse<MuteState>(
(await _metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value, out var muteState) (await metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value, out var muteState)
? muteState ? muteState
: null; : null;
@ -195,7 +187,7 @@ public class MuteManager
if (clientMuteMeta is not null) return clientMuteMeta; if (clientMuteMeta is not null) return clientMuteMeta;
// Get meta from database and store in client if exists // Get meta from database and store in client if exists
clientMuteMeta = await _metaService.GetPersistentMetaValue<MuteStateMeta>(Plugin.MuteKey, client.ClientId); clientMuteMeta = await metaService.GetPersistentMetaValue<MuteStateMeta>(Plugin.MuteKey, client.ClientId);
if (clientMuteMeta is not null) client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta); if (clientMuteMeta is not null) client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
return clientMuteMeta; return clientMuteMeta;
@ -204,6 +196,6 @@ public class MuteManager
private async Task WritePersistentData(EFClient client, MuteStateMeta clientMuteMeta) private async Task WritePersistentData(EFClient client, MuteStateMeta clientMuteMeta)
{ {
client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta); client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
await _metaService.SetPersistentMetaValue(Plugin.MuteKey, clientMuteMeta, client.ClientId); await metaService.SetPersistentMetaValue(Plugin.MuteKey, clientMuteMeta, client.ClientId);
} }
} }

View File

@ -21,15 +21,14 @@ public class Plugin : IPluginV2
public const string MuteKey = "IW4MMute"; public const string MuteKey = "IW4MMute";
public static IManager Manager { get; private set; } = null!; public static IManager Manager { get; private set; } = null!;
public static Server.Game[] SupportedGames { get; private set; } = Array.Empty<Server.Game>(); public static Server.Game[] SupportedGames { get; private set; } = [];
private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"}; private static readonly string[] DisabledCommands = [nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"];
private readonly IInteractionRegistration _interactionRegistration; private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService; private readonly IRemoteCommandService _remoteCommandService;
private readonly MuteManager _muteManager; private readonly MuteManager _muteManager;
private const string MuteInteraction = "Webfront::Profile::Mute"; private const string MuteInteraction = "Webfront::Profile::Mute";
public Plugin(IInteractionRegistration interactionRegistration, public Plugin(IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService, MuteManager muteManager)
IRemoteCommandService remoteCommandService, MuteManager muteManager)
{ {
_interactionRegistration = interactionRegistration; _interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService; _remoteCommandService = remoteCommandService;
@ -72,7 +71,7 @@ public class Plugin : IPluginV2
return !DisabledCommands.Contains(command.GetType().Name) && !command.IsBroadcast; 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)) if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value))
{ {
@ -84,12 +83,12 @@ public class Plugin : IPluginV2
.MuteState; .MuteState;
var server = manager.GetServers().First(); 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 return clientMuteMetaState is MuteState.Unmuted or MuteState.Unmuting
? CreateMuteInteraction(targetClientId.Value, server, GetCommandName) ? CreateMuteInteraction(targetClientId.Value, server, GetCommandName)
: CreateUnmuteInteraction(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; return Task.CompletedTask;
} }
@ -108,9 +107,9 @@ public class Plugin : IPluginV2
} }
var networkIds = updateEvent.Clients.Select(client => client.NetworkId).ToList(); 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); var muteMetaUpdate = await _muteManager.GetCurrentMuteState(client);
if (!muteMetaUpdate.CommandExecuted) if (!muteMetaUpdate.CommandExecuted)
@ -136,7 +135,7 @@ public class Plugin : IPluginV2
{ {
var muteMetaSay = await _muteManager.GetCurrentMuteState(messageEvent.Origin); 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. // Let the client know when their mute expires.
messageEvent.Origin.Tell(Utilities.CurrentLocalization messageEvent.Origin.Tell(Utilities.CurrentLocalization
@ -190,6 +189,29 @@ public class Plugin : IPluginV2
Values = (Dictionary<string, string>?)null Values = (Dictionary<string, string>?)null
}; };
var presetReasonInput = new
{
Name = "PresetReason",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_PRESET_REASON"],
Type = "select",
Values = (Dictionary<string, string>?)new Dictionary<string, string>
{
{ 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 var durationInput = new
{ {
Name = "Duration", Name = "Duration",
@ -206,7 +228,7 @@ public class Plugin : IPluginV2
} }
}; };
var inputs = new[] {reasonInput, durationInput}; var inputs = new[] { reasonInput, presetReasonInput, durationInput };
var inputsJson = JsonSerializer.Serialize(inputs); var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData return new InteractionData
@ -215,7 +237,7 @@ public class Plugin : IPluginV2
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"], Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"],
DisplayMeta = "oi-volume-off", DisplayMeta = "oi-volume-off",
ActionPath = "DynamicAction", ActionPath = "DynamicAction",
ActionMeta = new() ActionMeta = new Dictionary<string, string>
{ {
{ "InteractionId", MuteInteraction }, { "InteractionId", MuteInteraction },
{ "Inputs", inputsJson }, { "Inputs", inputsJson },
@ -249,11 +271,14 @@ public class Plugin : IPluginV2
args.Add(duration); 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 = var commandResponse =
await _remoteCommandService.Execute(originId, targetId, muteCommand, args, server); await _remoteCommandService.Execute(originId, targetId, muteCommand, args, server);
return string.Join(".", commandResponse.Select(result => result.Response)); return string.Join(".", commandResponse.Select(result => result.Response));
@ -277,11 +302,10 @@ public class Plugin : IPluginV2
return new InteractionData return new InteractionData
{ {
EntityId = targetClientId, EntityId = targetClientId,
Name = Utilities.CurrentLocalization.LocalizationIndex[ Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
DisplayMeta = "oi-volume-high", DisplayMeta = "oi-volume-high",
ActionPath = "DynamicAction", ActionPath = "DynamicAction",
ActionMeta = new() ActionMeta = new Dictionary<string, string>
{ {
{ "InteractionId", MuteInteraction }, { "InteractionId", MuteInteraction },
{ "Outputs", reasonInput.Name }, { "Outputs", reasonInput.Name },

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ApplicationIcon /> <ApplicationIcon />
<StartupObject /> <StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId> <PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
@ -16,11 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project> </Project>

View File

@ -233,7 +233,7 @@ const plugin = {
// todo: refactor to mapping if possible // todo: refactor to mapping if possible
if (event.eventType === 'ClientDataRequested') { if (event.eventType === 'ClientDataRequested') {
const client = server.getClientByNumber(event.clientNumber); const client = server.connectedClients[event.clientNumber];
if (client != null) { if (client != null) {
this.logger.logDebug('Found client {name}', client.name); this.logger.logDebug('Found client {name}', client.name);
@ -269,8 +269,9 @@ const plugin = {
} }
} }
let _;
if (event.eventType === 'SetClientDataRequested') { if (event.eventType === 'SetClientDataRequested') {
let client = server.getClientByNumber(event.clientNumber); let client = server.connectedClients[event.clientNumber];
let clientId; let clientId;
if (client != null) { if (client != null) {
@ -298,12 +299,12 @@ const plugin = {
const parsedValue = parseInt(event.data['value']); const parsedValue = parseInt(event.data['value']);
const key = event.data['key'].toString(); const key = event.data['key'].toString();
if (!isNaN(parsedValue)) { if (!isNaN(parsedValue)) {
event.data['direction'] = 'up' ? _ = event.data['direction'] === 'increment' ?
(await metaService.incrementPersistentMeta(key, parsedValue, clientId, token)).result : (await metaService.incrementPersistentMeta(key, parsedValue, clientId, token)).result :
(await metaService.decrementPersistentMeta(key, parsedValue, clientId, token)).result; (await metaService.decrementPersistentMeta(key, parsedValue, clientId, token)).result;
} }
} else { } 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') { if (event.data['key'] === 'PersistentClientGuid') {

View File

@ -16,7 +16,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Kick = 'kickClient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.Kick = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = '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.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.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *'; rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';

View File

@ -9,7 +9,7 @@ const serverOrderCache = [];
const plugin = { const plugin = {
author: 'RaidMax', author: 'RaidMax',
version: '1.0', version: '1.1',
name: 'Server Banner', name: 'Server Banner',
serviceResolver: null, serviceResolver: null,
scriptHelper: null, scriptHelper: null,
@ -26,6 +26,9 @@ const plugin = {
this.manager = serviceResolver.resolveService('IManager'); this.manager = serviceResolver.resolveService('IManager');
this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']); this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
this.webfrontUrl = serviceResolver.resolveService('ApplicationConfiguration').webfrontUrl; this.webfrontUrl = serviceResolver.resolveService('ApplicationConfiguration').webfrontUrl;
this.logger.logInformation('{Name} {Version} by {Author} loaded,', this.name, this.version,
this.author);
}, },
onServerMonitoringStart: function (startEvent) { onServerMonitoringStart: function (startEvent) {
@ -123,11 +126,13 @@ const plugin = {
}, },
}; };
plugin.manager.getServers().forEach(eachServer => { const servers = plugin.manager.servers;
if (eachServer.id === serverId) { for (let i = 0; i < servers.length; i++) {
server = eachServer; if (servers[i].id === serverId) {
server = servers[i];
break;
}
} }
});
if (serverLocationCache[server.listenAddress] === undefined) { if (serverLocationCache[server.listenAddress] === undefined) {
plugin.onServerMonitoringStart({ plugin.onServerMonitoringStart({
@ -280,7 +285,7 @@ const plugin = {
<div class="server-container small" id="server"> <div class="server-container small" id="server">
<div class="first-line small"> <div class="first-line small">
<div class="game-icon small"></div> <div class="game-icon small"></div>
<div class="header" style="${colorLeft}">${server.serverName.stripColors()}</div> <div class="header" id="serverName" style="${colorLeft}"></div>
</div> </div>
<div class="third-line game-info small"> <div class="third-line game-info small">
${status} ${status}
@ -298,6 +303,10 @@ const plugin = {
</div> </div>
</div> </div>
</div> </div>
<script>
const serverNameElem = document.getElementById('serverName');
serverNameElem.textContent = '${server.serverName.stripColors()}';
</script>
</body> </body>
</html>`; </html>`;
} }
@ -310,7 +319,7 @@ const plugin = {
style="background: url('https://raidmax.org/resources/images/icons/games/${gameCode}.jpg');"> style="background: url('https://raidmax.org/resources/images/icons/games/${gameCode}.jpg');">
</div> </div>
<div style="flex: 1; ${colorLeft}" class="game-info large"> <div style="flex: 1; ${colorLeft}" class="game-info large">
<div class="header">${server.serverName.stripColors()}</div> <div class="header" id="serverName"></div>
<div class="text-weight-lighter subtitle">${displayIp}:${server.listenPort}</div> <div class="text-weight-lighter subtitle">${displayIp}:${server.listenPort}</div>
<div class="players-flag-section"> <div class="players-flag-section">
<div class="subtitle">${server.throttled ? '-' : server.clientNum}/${server.maxClients} Players</div> <div class="subtitle">${server.throttled ? '-' : server.clientNum}/${server.maxClients} Players</div>
@ -324,6 +333,10 @@ const plugin = {
${status} ${status}
</div> </div>
</div> </div>
<script>
const serverNameElem = document.getElementById('serverName');
serverNameElem.textContent = '${server.serverName.stripColors()}';
</script>
</body> </body>
</html>`; </html>`;
}; };
@ -346,22 +359,24 @@ const plugin = {
interactionData.scriptAction = (_, __, ___, ____, _____) => { interactionData.scriptAction = (_, __, ___, ____, _____) => {
if (Object.keys(serverOrderCache).length === 0) { 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({ plugin.onServerMonitoringStart({
server: server server: server
}); });
}); }
} }
let response = '<div class="d-flex flex-row flex-wrap" style="margin-left: -1rem; margin-top: -1rem;">'; let response = '<div class="d-flex flex-row flex-wrap" style="margin-left: -1rem; margin-top: -1rem;">';
Object.keys(serverOrderCache).forEach(key => { Object.keys(serverOrderCache).forEach(key => {
const servers = serverOrderCache[key]; const servers = serverOrderCache[key];
servers.forEach(eachServer => { for (let i = 0; i < servers.length; i++) {
const eachServer = servers[i];
response += `<div class="w-full w-xl-half"> response += `<div class="w-full w-xl-half">
<div class="card m-10 p-20"> <div class="card m-10 p-20">
<div class="font-size-16 mb-10"> <div class="font-size-16 mb-10">
<div class="badge ml-10 float-right font-size-16">${eachServer.gameCode}</div> <div class="badge ml-10 float-right font-size-16">${eachServer.gameCode}</div>
${eachServer.serverName.stripColors()} <div id="serverName"></div>
</div> </div>
<div style="overflow: hidden"> <div style="overflow: hidden">
@ -387,8 +402,12 @@ const plugin = {
<br/>&nbsp;width="400" height="70" style="border-width: 0; overflow: hidden;"&gt;<br/> <br/>&nbsp;width="400" height="70" style="border-width: 0; overflow: hidden;"&gt;<br/>
&lt;/iframe&gt;</div> &lt;/iframe&gt;</div>
</div> </div>
</div>`; </div>
}); <script>
const serverNameElem = document.getElementById('serverName');
serverNameElem.textContent = '${eachServer.serverName.stripColors()}';
</script>`;
}
}); });
response += '</div>'; response += '</div>';

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ApplicationIcon /> <ApplicationIcon />
<StartupObject /> <StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId> <PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
@ -17,11 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project> </Project>

View File

@ -6,8 +6,8 @@ using SharedLibraryCore.Database.Models;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading; using System.Threading;
using Newtonsoft.Json.Linq;
using Humanizer; using Humanizer;
using Data.Abstractions; using Data.Abstractions;
using Data.Models; using Data.Models;
@ -108,8 +108,9 @@ public class Plugin : IPluginV2
var response = var response =
await wc.GetStringAsync(new Uri( await wc.GetStringAsync(new Uri(
$"http://ip-api.com/json/{ip}?lang={Utilities.CurrentLocalization.LocalizationName.Split("-").First().ToLower()}")); $"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) return string.IsNullOrEmpty(response)
? Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_UNKNOWN_COUNTRY"] ? Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_UNKNOWN_COUNTRY"]

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ApplicationIcon /> <ApplicationIcon />
<StartupObject /> <StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Welcome</PackageId> <PackageId>RaidMax.IW4MAdmin.Plugins.Welcome</PackageId>
@ -15,12 +15,8 @@
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.6.22.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -2,8 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization;
using Data.Models.Misc; using Data.Models.Misc;
using Newtonsoft.Json;
using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
@ -22,7 +22,7 @@ namespace SharedLibraryCore.Configuration
public bool EnableWebFront { get; set; } public bool EnableWebFront { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_BIND_URL")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_BIND_URL")]
public string WebfrontBindUrl { get; set; } public string WebfrontBindUrl { get; set; } = "http://0.0.0.0:1624";
[ConfigurationOptional] [ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_MANUAL_URL")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_MANUAL_URL")]

View File

@ -1,11 +1,11 @@
using System; using System;
using Newtonsoft.Json; using System.Text.Json.Serialization;
using Newtonsoft.Json.Converters; using SharedLibraryCore.Helpers;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Configuration namespace SharedLibraryCore.Configuration;
{
/// <summary> /// <summary>
/// Config driven command properties /// Config driven command properties
/// </summary> /// </summary>
@ -24,7 +24,7 @@ namespace SharedLibraryCore.Configuration
/// <summary> /// <summary>
/// Specifies the minimum permission level needed to execute the /// Specifies the minimum permission level needed to execute the
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]
public Permission MinimumPermission { get; set; } public Permission MinimumPermission { get; set; }
/// <summary> /// <summary>
@ -35,7 +35,6 @@ namespace SharedLibraryCore.Configuration
/// <summary> /// <summary>
/// Specifies the games supporting the functionality of the command /// Specifies the games supporting the functionality of the command
/// </summary> /// </summary>
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))] [JsonConverter(typeof(GameArrayJsonConverter))]
public Game[] SupportedGames { get; set; } = Array.Empty<Game>(); public Game[] SupportedGames { get; set; } = Array.Empty<Game>();
} }
}

View File

@ -9,16 +9,9 @@ namespace SharedLibraryCore.Configuration
{ {
public class ServerConfiguration : IBaseConfiguration public class ServerConfiguration : IBaseConfiguration
{ {
private readonly IList<IRConParser> _rconParsers; private readonly IList<IRConParser> _rconParsers = new List<IRConParser>();
private IRConParser _selectedParser; private IRConParser _selectedParser;
public ServerConfiguration()
{
_rconParsers = new List<IRConParser>();
Rules = new string[0];
AutoMessages = new string[0];
}
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_IP")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_IP")]
public string IPAddress { get; set; } public string IPAddress { get; set; }
@ -29,10 +22,10 @@ namespace SharedLibraryCore.Configuration
public string Password { get; set; } public string Password { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_RULES")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_RULES")]
public string[] Rules { get; set; } = new string[0]; public string[] Rules { get; set; } = [];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_AUTO_MESSAGES")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_AUTO_MESSAGES")]
public string[] AutoMessages { get; set; } = new string[0]; public string[] AutoMessages { get; set; } = [];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_PATH")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SERVER_PATH")]
[ConfigurationOptional] [ConfigurationOptional]
@ -88,7 +81,7 @@ namespace SharedLibraryCore.Configuration
var passwords = _selectedParser.TryGetRConPasswords(); var passwords = _selectedParser.TryGetRConPasswords();
if (passwords.Length > 1) if (passwords.Length > 1)
{ {
var (index, value) = var (index, _) =
loc["SETUP_RCON_PASSWORD_PROMPT"].PromptSelection(loc["SETUP_RCON_PASSWORD_MANUAL"], null, loc["SETUP_RCON_PASSWORD_PROMPT"].PromptSelection(loc["SETUP_RCON_PASSWORD_MANUAL"], null,
passwords.Select(pw => passwords.Select(pw =>
$"{pw.Item1}{(string.IsNullOrEmpty(pw.Item2) ? "" : " " + pw.Item2)}") $"{pw.Item1}{(string.IsNullOrEmpty(pw.Item2) ? "" : " " + pw.Item2)}")
@ -113,9 +106,8 @@ namespace SharedLibraryCore.Configuration
Password = loc["SETUP_SERVER_RCON"].PromptString(); Password = loc["SETUP_SERVER_RCON"].PromptString();
} }
AutoMessages = new string[0]; AutoMessages = [];
Rules = new string[0]; Rules = [];
ManualLogPath = null;
return this; return this;
} }

View File

@ -6,7 +6,7 @@ public abstract class CoreEvent
{ {
public Guid Id { get; } = Guid.NewGuid(); public Guid Id { get; } = Guid.NewGuid();
public Guid? CorrelationId { get; init; } public Guid? CorrelationId { get; init; }
public object Source { get; init; } public object Source { get; set; }
public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow;
public DateTimeOffset? ProcessedAt { get; set; } public DateTimeOffset? ProcessedAt { get; set; }
} }

View File

@ -0,0 +1,6 @@
namespace SharedLibraryCore.Events.Game;
public class GameLogEvent : GameEventV2
{
public string LogLine { get; set; }
}

View File

@ -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++;
}
}
} }

View File

@ -1,5 +1,5 @@
using Data.Models; using Data.Models;
using Data.Models.Client; using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Events.Management; namespace SharedLibraryCore.Events.Management;

View File

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

View File

@ -1,27 +1,58 @@
using System; using System;
using Newtonsoft.Json; using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SharedLibraryCore.Helpers;
namespace SharedLibraryCore.Helpers
{
/// <summary> /// <summary>
/// JSON converter for the build number /// JSON converter for the build number
/// </summary> /// </summary>
public class BuildNumberJsonConverter : JsonConverter public class BuildNumberJsonConverter : JsonConverter<BuildNumber>
{ {
public override bool CanConvert(Type objectType) public override BuildNumber Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
return objectType == typeof(string); var stringValue = reader.GetString();
return BuildNumber.Parse(stringValue);
} }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, public override void Write(Utf8JsonWriter writer, BuildNumber value, JsonSerializerOptions options)
JsonSerializer serializer)
{ {
return BuildNumber.Parse(reader.Value.ToString()); writer.WriteStringValue(value.ToString());
}
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public class GameArrayJsonConverter : JsonConverter<Server.Game[]>
{ {
writer.WriteValue(value.ToString()); public override Server.Game[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
} {
List<Server.Game> games = [];
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
var gameString = reader.GetString();
var game = Enum.Parse<Server.Game>(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();
} }
} }

View File

@ -86,6 +86,11 @@ public interface IGameEventSubscriptions
/// </summary> /// </summary>
static event Func<GameScriptEvent, CancellationToken, Task> ScriptEventTriggered; static event Func<GameScriptEvent, CancellationToken, Task> ScriptEventTriggered;
/// <summary>
/// Raised when game log prints a line that is not handled by any other cases
/// </summary>
static event Func<GameLogEvent, CancellationToken, Task> GameLogEventTriggered;
static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token) static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
{ {
return coreEvent switch return coreEvent switch
@ -100,6 +105,7 @@ public interface IGameEventSubscriptions
ClientCommandEvent clientCommandEvent => ClientEnteredCommand?.InvokeAsync(clientCommandEvent, token) ?? Task.CompletedTask, ClientCommandEvent clientCommandEvent => ClientEnteredCommand?.InvokeAsync(clientCommandEvent, token) ?? Task.CompletedTask,
ClientMessageEvent clientMessageEvent => ClientMessaged?.InvokeAsync(clientMessageEvent, token) ?? Task.CompletedTask, ClientMessageEvent clientMessageEvent => ClientMessaged?.InvokeAsync(clientMessageEvent, token) ?? Task.CompletedTask,
GameScriptEvent gameScriptEvent => ScriptEventTriggered?.InvokeAsync(gameScriptEvent, token) ?? Task.CompletedTask, GameScriptEvent gameScriptEvent => ScriptEventTriggered?.InvokeAsync(gameScriptEvent, token) ?? Task.CompletedTask,
GameLogEvent gameLogEvent => GameLogEventTriggered?.InvokeAsync(gameLogEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask _ => Task.CompletedTask
}; };
} }
@ -116,5 +122,6 @@ public interface IGameEventSubscriptions
ClientMessaged = null; ClientMessaged = null;
ClientEnteredCommand = null; ClientEnteredCommand = null;
ScriptEventTriggered = null; ScriptEventTriggered = null;
GameLogEventTriggered = null;
} }
} }

View File

@ -0,0 +1,8 @@
namespace SharedLibraryCore.Interfaces.Events;
public interface IGameScriptEvent
{
string ScriptData { get; set; }
string EventName { get; }
void ParseArguments();
}

View File

@ -0,0 +1,6 @@
namespace SharedLibraryCore.Interfaces.Events;
public interface IGameScriptEventFactory
{
IGameScriptEvent Create(string eventType, string logData);
}

View File

@ -73,6 +73,11 @@ public interface IGameServerEventSubscriptions
/// </summary> /// </summary>
static event Func<ServerValueSetCompleteEvent, CancellationToken, Task> ServerValueSetCompleted; static event Func<ServerValueSetCompleteEvent, CancellationToken, Task> ServerValueSetCompleted;
/// <summary>
/// Raised when a server's status response is received
/// </summary>
static event Func<ServerStatusReceiveEvent, CancellationToken, Task> ServerStatusReceived;
static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token) static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
{ {
return coreEvent switch return coreEvent switch
@ -88,6 +93,7 @@ public interface IGameServerEventSubscriptions
ServerValueReceiveEvent serverValueReceiveEvent => ServerValueReceived?.InvokeAsync(serverValueReceiveEvent, token) ?? Task.CompletedTask, ServerValueReceiveEvent serverValueReceiveEvent => ServerValueReceived?.InvokeAsync(serverValueReceiveEvent, token) ?? Task.CompletedTask,
ServerValueSetRequestEvent serverValueSetRequestEvent => ServerValueSetRequested?.InvokeAsync(serverValueSetRequestEvent, token) ?? Task.CompletedTask, ServerValueSetRequestEvent serverValueSetRequestEvent => ServerValueSetRequested?.InvokeAsync(serverValueSetRequestEvent, token) ?? Task.CompletedTask,
ServerValueSetCompleteEvent serverValueSetCompleteEvent => ServerValueSetCompleted?.InvokeAsync(serverValueSetCompleteEvent, token) ?? Task.CompletedTask, ServerValueSetCompleteEvent serverValueSetCompleteEvent => ServerValueSetCompleted?.InvokeAsync(serverValueSetCompleteEvent, token) ?? Task.CompletedTask,
ServerStatusReceiveEvent serverStatusReceiveEvent => ServerStatusReceived?.InvokeAsync(serverStatusReceiveEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask _ => Task.CompletedTask
}; };
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
@ -19,6 +20,9 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null); Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
IPEndPoint ResolvedIpEndPoint { get; }
IRConParser RconParser { get; }
/// <summary> /// <summary>
/// Execute a server command /// Execute a server command
/// </summary> /// </summary>

View File

@ -41,6 +41,7 @@ namespace SharedLibraryCore.Interfaces
ILogger GetLogger(long serverId); ILogger GetLogger(long serverId);
IList<Server> GetServers(); IList<Server> GetServers();
List<Server> Servers { get; }
IList<IManagerCommand> GetCommands(); IList<IManagerCommand> GetCommands();
IList<MessageToken> GetMessageTokens(); IList<MessageToken> GetMessageTokens();
IList<EFClient> GetActiveClients(); IList<EFClient> GetActiveClients();

View File

@ -15,35 +15,35 @@ namespace SharedLibraryCore.Interfaces
/// <summary> /// <summary>
/// stores the game/client specific version (usually the value of the "version" DVAR) /// stores the game/client specific version (usually the value of the "version" DVAR)
/// </summary> /// </summary>
string Version { get; } string Version { get; set; }
/// <summary> /// <summary>
/// specifies the game name (usually the internal studio iteration ie: IW4, T5 etc...) /// specifies the game name (usually the internal studio iteration ie: IW4, T5 etc...)
/// </summary> /// </summary>
Game GameName { get; } Game GameName { get; set; }
/// <summary> /// <summary>
/// indicates if the game supports generating a log path from DVAR retrieval /// indicates if the game supports generating a log path from DVAR retrieval
/// of fs_game, fs_basepath, g_log /// of fs_game, fs_basepath, g_log
/// </summary> /// </summary>
bool CanGenerateLogPath { get; } bool CanGenerateLogPath { get; set; }
/// <summary> /// <summary>
/// specifies the name of the parser /// specifies the name of the parser
/// </summary> /// </summary>
string Name { get; } string Name { get; set; }
/// <summary> /// <summary>
/// specifies the type of rcon engine /// specifies the type of rcon engine
/// eg: COD, Source /// eg: COD, Source
/// </summary> /// </summary>
string RConEngine { get; } string RConEngine { get; set; }
/// <summary> /// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded), /// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory /// but rather always to the fs_basegame directory
/// </summary> /// </summary>
bool IsOneLog { get; } bool IsOneLog { get; set; }
/// <summary> /// <summary>
/// retrieves the value of a given DVAR /// retrieves the value of a given DVAR
@ -54,7 +54,8 @@ namespace SharedLibraryCore.Interfaces
/// <param name="fallbackValue">default value to return if dvar retrieval fails</param> /// <param name="fallbackValue">default value to return if dvar retrieval fails</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default); Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default,
CancellationToken token = default);
/// <summary> /// <summary>
/// set value of DVAR by name /// set value of DVAR by name

View File

@ -10,74 +10,74 @@ namespace SharedLibraryCore.Interfaces
/// <summary> /// <summary>
/// stores the command format for console commands /// stores the command format for console commands
/// </summary> /// </summary>
CommandPrefix CommandPrefixes { get; } CommandPrefix CommandPrefixes { get; set; }
/// <summary> /// <summary>
/// stores the regex info for parsing get status response /// stores the regex info for parsing get status response
/// </summary> /// </summary>
ParserRegex Status { get; } ParserRegex Status { get; set; }
/// <summary> /// <summary>
/// stores regex info for parsing the map line from rcon status response /// stores regex info for parsing the map line from rcon status response
/// </summary> /// </summary>
ParserRegex MapStatus { get; } ParserRegex MapStatus { get; set; }
/// <summary> /// <summary>
/// stores regex info for parsing the gametype line from rcon status response /// stores regex info for parsing the gametype line from rcon status response
/// </summary> /// </summary>
ParserRegex GametypeStatus { get; } ParserRegex GametypeStatus { get; set; }
/// <summary> /// <summary>
/// stores regex info for parsing hostname line from rcon status response /// stores regex info for parsing hostname line from rcon status response
/// </summary> /// </summary>
ParserRegex HostnameStatus { get; } ParserRegex HostnameStatus { get; set; }
/// <summary> /// <summary>
/// stores regex info for parsing max players line from rcon status response /// stores regex info for parsing max players line from rcon status response
/// </summary> /// </summary>
ParserRegex MaxPlayersStatus { get; } ParserRegex MaxPlayersStatus { get; set; }
/// <summary> /// <summary>
/// stores the regex info for parsing get DVAR responses /// stores the regex info for parsing get DVAR responses
/// </summary> /// </summary>
ParserRegex Dvar { get; } ParserRegex Dvar { get; set; }
/// <summary> /// <summary>
/// stores the regex info for parsing the header of a status response /// stores the regex info for parsing the header of a status response
/// </summary> /// </summary>
ParserRegex StatusHeader { get; } ParserRegex StatusHeader { get; set; }
/// <summary> /// <summary>
/// Specifies the expected response message from rcon when the server is not running /// Specifies the expected response message from rcon when the server is not running
/// </summary> /// </summary>
string ServerNotRunningResponse { get; } string ServerNotRunningResponse { get; set; }
/// <summary> /// <summary>
/// indicates if the application should wait for response from server /// indicates if the application should wait for response from server
/// when executing a command /// when executing a command
/// </summary> /// </summary>
bool WaitForResponse { get; } bool WaitForResponse { get; set; }
/// <summary> /// <summary>
/// indicates the format expected for parsed guids /// indicates the format expected for parsed guids
/// </summary> /// </summary>
NumberStyles GuidNumberStyle { get; } NumberStyles GuidNumberStyle { get; set; }
/// <summary> /// <summary>
/// specifies simple mappings for dvar names in scenarios where the needed /// specifies simple mappings for dvar names in scenarios where the needed
/// information is not stored in a traditional dvar name /// information is not stored in a traditional dvar name
/// </summary> /// </summary>
IDictionary<string, string> OverrideDvarNameMapping { get; } IDictionary<string, string> OverrideDvarNameMapping { get; set; }
/// <summary> /// <summary>
/// specifies the default dvar values for games that don't support certain dvars /// specifies the default dvar values for games that don't support certain dvars
/// </summary> /// </summary>
IDictionary<string, string> DefaultDvarValues { get; } IDictionary<string, string> DefaultDvarValues { get; set; }
/// <summary> /// <summary>
/// contains a setup of commands that have override timeouts /// contains a setup of commands that have override timeouts
/// </summary> /// </summary>
IDictionary<string, int?> OverrideCommandTimeouts { get; } IDictionary<string, int?> OverrideCommandTimeouts { get; set; }
/// <summary> /// <summary>
/// specifies how many lines can be used for ingame notice /// specifies how many lines can be used for ingame notice
@ -87,29 +87,30 @@ namespace SharedLibraryCore.Interfaces
/// <summary> /// <summary>
/// specifies how many characters can be displayed per notice line /// specifies how many characters can be displayed per notice line
/// </summary> /// </summary>
int NoticeMaxCharactersPerLine { get; } int NoticeMaxCharactersPerLine { get; set; }
/// <summary> /// <summary>
/// specifies the characters used to split a line /// specifies the characters used to split a line
/// </summary> /// </summary>
string NoticeLineSeparator { get; } string NoticeLineSeparator { get; set; }
/// <summary> /// <summary>
/// Default port the game listens to RCon requests on /// Default port the game listens to RCon requests on
/// </summary> /// </summary>
int? DefaultRConPort { get; } int? DefaultRConPort { get; set; }
/// <summary> /// <summary>
/// Default Indicator of where the game is installed (ex file path or registry entry) /// Default Indicator of where the game is installed (ex file path or registry entry)
/// </summary> /// </summary>
string DefaultInstallationDirectoryHint { get; } string DefaultInstallationDirectoryHint { get; set; }
ColorCodeMapping ColorCodeMapping { get; } ColorCodeMapping ColorCodeMapping { get; set; }
short FloodProtectInterval { get; set; }
short FloodProtectInterval { get; }
/// <summary> /// <summary>
/// indicates if diacritics (accented characters) should be normalized /// indicates if diacritics (accented characters) should be normalized
/// </summary> /// </summary>
bool ShouldRemoveDiacritics { get; } bool ShouldRemoveDiacritics { get; set; }
} }
} }

View File

@ -31,5 +31,10 @@ namespace SharedLibraryCore.Interfaces
/// active clients /// active clients
/// </summary> /// </summary>
EFClient[] Clients { get; } EFClient[] Clients { get; }
/// <summary>
/// raw text data from the game server
/// </summary>
string[] RawResponse { get; }
} }
} }

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text.Json.Serialization;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Localization namespace SharedLibraryCore.Localization
@ -8,6 +9,8 @@ namespace SharedLibraryCore.Localization
{ {
private string localizationName; private string localizationName;
public Layout() { }
public Layout(Dictionary<string, string> set) public Layout(Dictionary<string, string> set)
{ {
LocalizationIndex = new TranslationLookup LocalizationIndex = new TranslationLookup
@ -27,7 +30,7 @@ namespace SharedLibraryCore.Localization
} }
public TranslationLookup LocalizationIndex { get; set; } public TranslationLookup LocalizationIndex { get; set; }
public CultureInfo Culture { get; private set; } [JsonIgnore] public CultureInfo Culture { get; private set; }
} }
public class TranslationLookup : ITranslationLookup public class TranslationLookup : ITranslationLookup

View File

@ -120,6 +120,11 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] [NotMapped]
public string TimeSinceLastConnectionString => (DateTime.UtcNow - LastConnection).HumanizeForCurrentCulture(); public string TimeSinceLastConnectionString => (DateTime.UtcNow - LastConnection).HumanizeForCurrentCulture();
public DateTimeOffset LastCommandExecutionAttempt { get; set; } = DateTimeOffset.MinValue;
[NotMapped]
public int CommandExecutionAttempts { get; set; }
[NotMapped] [NotMapped]
// this is kinda dirty, but I need localizable level names // this is kinda dirty, but I need localizable level names
public ClientPermission ClientPermission => new ClientPermission public ClientPermission ClientPermission => new ClientPermission

View File

@ -434,6 +434,7 @@ namespace SharedLibraryCore
public abstract Task<long> GetIdForServer(Server server = null); public abstract Task<long> GetIdForServer(Server server = null);
[Obsolete("Use the ScriptPluginExtension helper")]
public EFClient GetClientByNumber(int clientNumber) => public EFClient GetClientByNumber(int clientNumber) =>
GetClientsAsList().FirstOrDefault(client => client.ClientNumber == clientNumber); GetClientsAsList().FirstOrDefault(client => client.ClientNumber == clientNumber);
} }

View File

@ -2,9 +2,9 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2023.4.5.1</Version> <Version>2024.01.01.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2023.4.5.1</PackageVersion> <PackageVersion>2024.06.22.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup> </PropertyGroup>
@ -34,23 +34,22 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="11.2.1" /> <PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" /> <PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.de" Version="2.14.1" /> <PackageReference Include="Humanizer.Core.de" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.es" Version="2.14.1" /> <PackageReference Include="Humanizer.Core.es" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.pt" Version="2.14.1" /> <PackageReference Include="Humanizer.Core.pt" Version="2.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.8" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" /> <PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup> </ItemGroup>
@ -60,11 +59,6 @@
<IncludeAssets>Data.dll</IncludeAssets> <IncludeAssets>Data.dll</IncludeAssets>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="if not exist &quot;$(ProjectDir)..\BUILD&quot; (&#xD;&#xA;if $(ConfigurationName) == Debug (&#xD;&#xA;md &quot;$(ProjectDir)..\BUILD&quot;&#xD;&#xA;)&#xD;&#xA;)&#xD;&#xA;if not exist &quot;$(ProjectDir)..\BUILD\Plugins&quot; (&#xD;&#xA;if $(ConfigurationName) == Debug (&#xD;&#xA;md &quot;$(ProjectDir)..\BUILD\Plugins&quot;&#xD;&#xA;)&#xD;&#xA;)" />
</Target>
<PropertyGroup> <PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput> <TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
</PropertyGroup> </PropertyGroup>

View File

@ -868,7 +868,14 @@ namespace SharedLibraryCore
{ {
try 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) catch (TaskCanceledException)
@ -1333,6 +1340,14 @@ namespace SharedLibraryCore
return serviceCollection; 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<CancellationToken, Task> action, CancellationToken token = default) => public static void ExecuteAfterDelay(TimeSpan duration, Func<CancellationToken, Task> action, CancellationToken token = default) =>
ExecuteAfterDelay((int)duration.TotalMilliseconds, action, token); ExecuteAfterDelay((int)duration.TotalMilliseconds, action, token);

View File

@ -10,10 +10,9 @@ using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WebfrontCore.ViewModels; using WebfrontCore.ViewModels;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
@ -92,15 +91,15 @@ namespace WebfrontCore.Controllers
try try
{ {
var file = JObject.Parse(content); var jsonDocument = JsonDocument.Parse(content);
} }
catch (JsonReaderException ex) catch (JsonException ex)
{ {
return BadRequest($"{fileName}: {ex.Message}"); return BadRequest($"{fileName}: {ex.Message}");
} }
var path = System.IO.Path.Join(Utilities.OperatingDirectory, "Configuration", var path = Path.Join(Utilities.OperatingDirectory, "Configuration",
fileName.Replace($"{System.IO.Path.DirectorySeparatorChar}", "")); fileName.Replace($"{Path.DirectorySeparatorChar}", ""));
// todo: move into a service at some point // todo: move into a service at some point
if (!System.IO.File.Exists(path)) if (!System.IO.File.Exists(path))

View File

@ -26,6 +26,11 @@ public class InteractionController : BaseController
return NotFound(); return NotFound();
} }
if (Client.Level < interactionData.MinimumPermission)
{
return Unauthorized();
}
ViewBag.Title = interactionData.Description; ViewBag.Title = interactionData.Description;
var meta = HttpContext.Request.Query.ToDictionary(key => key.Key, value => value.Value.ToString()); 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); var result = await _interactionRegistration.ProcessInteraction(interactionName, Client.ClientId, meta: meta, token: token);

View File

@ -1,10 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61369/",
"sslPort": 0
}
}
}

View File

@ -15,7 +15,7 @@
<environment include="Development"> <environment include="Development">
<link rel="stylesheet" href="~/lib/halfmoon/css/halfmoon-variables.css"/> <link rel="stylesheet" href="~/lib/halfmoon/css/halfmoon-variables.css"/>
<link rel="stylesheet" href="/css/src/main.css"/> <link rel="stylesheet" href="/css/src/main.css"/>
<link rel="stylesheet" href="/css/open-iconic.css"/> <link rel="stylesheet" href="/css/open-iconic-bootstrap-override.css"/>
</environment> </environment>
<environment include="Production"> <environment include="Production">
<link rel="stylesheet" href="~/css/global.min.css?version=@ViewBag.Version"/> <link rel="stylesheet" href="~/css/global.min.css?version=@ViewBag.Version"/>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild> <RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish> <RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc> <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
@ -33,22 +33,18 @@
<ItemGroup> <ItemGroup>
<Content Update="wwwroot\**\*.*" CopyToPublishDirectory="Never" /> <Content Update="wwwroot\**\*.*" CopyToPublishDirectory="Never" />
<Content Update="wwwroot\css\src\global.css" CopyToPublishDirectory="Never" />
<Content Update="wwwroot\css\src\global.min.css" CopyToPublishDirectory="Never" /> <Content Update="wwwroot\css\src\global.min.css" CopyToPublishDirectory="Never" />
<Content Update="Views\*.*" CopyToPublishDirectory="Never" /> <Content Update="Views\*.*" CopyToPublishDirectory="Never" />
<None Include="wwwroot\css\global.min.css" CopyToPublishDirectory="PreserveNewest" /> <None Include="wwwroot\css\global.min.css" CopyToPublishDirectory="PreserveNewest" />
<None Include="wwwroot\js\global.min.js" CopyToPublishDirectory="PreserveNewest" /> <None Include="wwwroot\js\global.min.js" CopyToPublishDirectory="PreserveNewest" />
<None Include="wwwroot\images\**\*.*" CopyToPublishDirectory="PreserveNewest" /> <None Include="wwwroot\images\**\*.*" CopyToPublishDirectory="PreserveNewest" />
<Content Remove="wwwroot\css\src\main.css.map" /> <None Remove="Properties\launchSettings.json" />
<Content Remove="dotnet-bundle.runtimeconfig.json" />
<Content Remove="dotnet-bundle.deps.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BuildWebCompiler2022" Version="1.14.10" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" /> <PackageReference Include="Microsoft.AspNetCore.ConcurrencyLimiter" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.ConcurrencyLimiter" Version="6.0.16" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.8" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" /> <PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
</ItemGroup> </ItemGroup>
@ -78,7 +74,4 @@
</VisualStudio> </VisualStudio>
</ProjectExtensions> </ProjectExtensions>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="if $(ConfigurationName) == Debug ( &#xA;powershell -Command wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o $(ProjectDir)wwwroot\lib\open-iconic\font\css\open-iconic-bootstrap-override.scss&#xA;echo d | xcopy /f /y $(ProjectDir)wwwroot\lib\open-iconic\font\fonts $(ProjectDir)wwwroot\font\&#xA;powershell -Command &quot;((Get-Content -path $(ProjectDir)wwwroot\lib\open-iconic\font\css\open-iconic-bootstrap-override.scss -Raw) -replace '../fonts/','/font/') | Set-Content -Path $(ProjectDir)wwwroot\lib\open-iconic\font\css\open-iconic-bootstrap-override.scss&quot;&#xA;)" />
</Target>
</Project> </Project>

View File

@ -3,9 +3,9 @@
"outputFileName": "wwwroot/css/global.min.css", "outputFileName": "wwwroot/css/global.min.css",
"inputFiles": [ "inputFiles": [
"wwwroot/lib/halfmoon/css/halfmoon-variables.min.css", "wwwroot/lib/halfmoon/css/halfmoon-variables.min.css",
"wwwroot/css/global.css",
"wwwroot/lib/chart.js/dist/Chart.min.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"
] ]
}, },
{ {

View File

@ -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"
}
]

View File

@ -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
}
}
}