1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-07 21:58:06 -05:00

Merge branch 'develop' into release/pre

This commit is contained in:
RaidMax 2024-06-30 12:09:13 -05:00
commit 4236ae5a6e
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
/Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal
bundle/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ using Data.Abstractions;
using Data.Context;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
@ -68,6 +69,7 @@ namespace IW4MAdmin.Application
private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
private readonly ConfigurationWatcher _watcher;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
@ -94,7 +96,8 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins,
ConfigurationWatcher watcher)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -102,10 +105,11 @@ namespace IW4MAdmin.Application
ClientSvc = clientService;
PenaltySvc = penaltyService;
_alertManager = alertManager;
_watcher = watcher;
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<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) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
@ -529,6 +533,7 @@ namespace IW4MAdmin.Application
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers();
_watcher.Enable();
IsInitialized = true;
}
@ -710,7 +715,7 @@ namespace IW4MAdmin.Application
public IEventParser GenerateDynamicEventParser(string name)
{
return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration())
return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration(), _serviceProvider.GetRequiredService<IGameScriptEventFactory>())
{
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 Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Game;
using SharedLibraryCore.Interfaces.Events;
using static System.Int32;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -21,16 +22,18 @@ namespace IW4MAdmin.Application.EventParsers
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
private readonly IGameScriptEventFactory _gameScriptEventFactory;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger,
ApplicationConfiguration appConfig)
ApplicationConfiguration appConfig, IGameScriptEventFactory gameScriptEventFactory)
{
_customEventRegistrations =
new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger;
_appConfig = appConfig;
_gameScriptEventFactory = gameScriptEventFactory;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
@ -183,14 +186,28 @@ namespace IW4MAdmin.Application.EventParsers
if (logLine.StartsWith("GSE;"))
{
return new GameScriptEvent
var gscEvent = new GameScriptEvent
{
ScriptData = logLine,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
return gscEvent;
}
var split = logLine.Split(";", StringSplitOptions.RemoveEmptyEntries);
if (split.Length > 1)
{
var createdEvent = _gameScriptEventFactory.Create(split[0], logLine.Replace(split[0], ""));
if (createdEvent is not null)
{
createdEvent.ParseArguments();
return createdEvent as GameEventV2;
}
}
if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey))
{
return GenerateDefaultEvent(logLine, gameTime);
@ -218,12 +235,13 @@ namespace IW4MAdmin.Application.EventParsers
return GenerateDefaultEvent(logLine, gameTime);
}
private static GameEvent GenerateDefaultEvent(string logLine, long gameTime)
private static GameLogEvent GenerateDefaultEvent(string logLine, long gameTime)
{
return new GameEvent
return new GameLogEvent
{
Type = GameEvent.EventType.Unknown,
Data = logLine,
LogLine = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,

View File

@ -1,5 +1,6 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers
@ -10,7 +11,9 @@ namespace IW4MAdmin.Application.EventParsers
/// </summary>
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.Linq;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Extensions;
@ -25,4 +27,16 @@ public static class ScriptPluginExtensions
{
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
}
public static EFClient GetClientByNumber(this IGameServer server, int clientNumber) =>
server.ConnectedClients.FirstOrDefault(client => client.ClientNumber == clientNumber);
public static EFClient GetClientByGuid(this IGameServer server, string clientGuid) =>
server.ConnectedClients.FirstOrDefault(client => client?.GuidString == clientGuid?.Trim().ToLower());
public static EFClient GetClientByXuid(this IGameServer server, string clientGuid) =>
server.ConnectedClients.FirstOrDefault(client => client?.XuidString == clientGuid?.Trim().ToLower());
public static EFClient GetClientByDecimalGuid(this IGameServer server, string clientGuid) =>
server.ConnectedClients.FirstOrDefault(client => client.NetworkId.ToString() == clientGuid?.Trim().ToLower());
}

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

View File

@ -20,7 +20,6 @@ public sealed class ConfigurationWatcher : IDisposable
};
_watcher.Changed += WatcherOnChanged;
_watcher.EnableRaisingEvents = true;
}
public void Dispose()
@ -31,30 +30,28 @@ public sealed class ConfigurationWatcher : IDisposable
public void Register(string fileName, Action<string> fileUpdated)
{
if (_registeredActions.ContainsKey(fileName))
{
return;
}
_registeredActions.Add(fileName, fileUpdated);
_registeredActions.TryAdd(fileName, fileUpdated);
}
public void Unregister(string fileName)
{
if (_registeredActions.ContainsKey(fileName))
{
_registeredActions.Remove(fileName);
}
_registeredActions.Remove(fileName);
}
public void Enable()
{
_watcher.EnableRaisingEvents = true;
}
private void WatcherOnChanged(object sender, FileSystemEventArgs eventArgs)
{
if (!_registeredActions.ContainsKey(eventArgs.FullPath) || eventArgs.ChangeType != WatcherChangeTypes.Changed ||
if (!_registeredActions.TryGetValue(eventArgs.FullPath, out var value) ||
eventArgs.ChangeType != WatcherChangeTypes.Changed ||
new FileInfo(eventArgs.FullPath).Length == 0)
{
return;
}
_registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath);
value.Invoke(eventArgs.FullPath);
}
}

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)
{
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)

View File

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

View File

@ -25,6 +25,7 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models;
using Data.Models.Server;
using Humanizer;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands;
using IW4MAdmin.Application.Plugin.Script;
@ -193,18 +194,54 @@ namespace IW4MAdmin
Command command = null;
if (E.Type == GameEvent.EventType.Command)
{
if (E.Origin is not null)
{
var canExecute = true;
if (E.Origin.CommandExecutionAttempts > 0 && E.Origin.Level < Permission.Trusted)
{
var remainingTimeout =
E.Origin.LastCommandExecutionAttempt +
Utilities.GetExponentialBackoffDelay(E.Origin.CommandExecutionAttempts) -
DateTimeOffset.UtcNow;
if (remainingTimeout.TotalSeconds > 0)
{
if (E.Origin.CommandExecutionAttempts < 2 ||
E.Origin.CommandExecutionAttempts % 5 == 0)
{
E.Origin.Tell(_translationLookup["COMMANDS_BACKOFF_MESSAGE"]
.FormatExt(remainingTimeout.Humanize()));
}
canExecute = false;
}
else
{
E.Origin.CommandExecutionAttempts = 0;
}
}
E.Origin.LastCommandExecutionAttempt = DateTimeOffset.UtcNow;
E.Origin.CommandExecutionAttempts++;
if (!canExecute)
{
return;
}
}
try
{
command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
}
catch (CommandException e)
{
ServerLogger.LogWarning(e, "Error validating command from event {@Event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid;
}
if (command != null)
{
E.Extra = command;

View File

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

View File

@ -5,7 +5,6 @@ using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
@ -39,6 +38,8 @@ using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Microsoft.Extensions.Hosting;
using Refit;
using SharedLibraryCore.Interfaces.Events;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
@ -94,15 +95,6 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("!!!! IMPORTANT !!!!");
Console.WriteLine("The next update of IW4MAdmin will require .NET 8.");
Console.WriteLine("This is a breaking change!");
Console.WriteLine(
"Please update the ASP.NET Core Runtime: https://dotnet.microsoft.com/en-us/download/dotnet/8.0");
Console.WriteLine("!!!!!!!!!!!!!!!!!!!");
Console.ForegroundColor = ConsoleColor.Gray;
await LaunchAsync();
}
@ -451,12 +443,12 @@ namespace IW4MAdmin.Application
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var httpClient = new HttpClient
var httpClient = new HttpClient(new HttpClientHandler {AllowAutoRedirect = true})
{
BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15)
};
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var masterRestClient = RestService.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null)
@ -469,10 +461,7 @@ namespace IW4MAdmin.Application
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
if (!Utilities.PermissionLevelOverrides.ContainsKey(key))
{
Utilities.PermissionLevelOverrides.Add(key, value);
}
Utilities.PermissionLevelOverrides.TryAdd(key, value);
}
// build the dependency list
@ -539,6 +528,7 @@ namespace IW4MAdmin.Application
.AddSingleton(new ConfigurationWatcher())
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton<IGameScriptEventFactory, GameScriptEventFactory>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);

View File

@ -1,5 +1,4 @@
using IW4MAdmin.Application.API.Master;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
@ -9,6 +8,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Refit;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
@ -28,6 +28,7 @@ namespace IW4MAdmin.Application.Misc
private readonly int _apiVersion = 1;
private bool _firstHeartBeat = true;
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
private string _authorizationToken;
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{
@ -128,7 +129,7 @@ namespace IW4MAdmin.Application.Misc
Id = _appConfig.Id
});
_apiInstance.AuthorizationToken = $"Bearer {token.AccessToken}";
_authorizationToken = $"Bearer {token.AccessToken}";
}
var instance = new ApiInstance
@ -153,22 +154,22 @@ namespace IW4MAdmin.Application.Misc
WebfrontUrl = _appConfig.WebfrontUrl
};
Response<ResultMessage> response;
IApiResponse<ResultMessage> response;
if (_firstHeartBeat)
{
response = await _apiInstance.AddInstance(instance);
response = await _apiInstance.AddInstance(instance, _authorizationToken);
}
else
{
response = await _apiInstance.UpdateInstance(instance.Id, instance);
response = await _apiInstance.UpdateInstance(instance.Id, instance, _authorizationToken);
_firstHeartBeat = false;
}
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.ResponseMessage.StatusCode, response.StringContent);
_logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.StatusCode, response.Error?.Content);
}
}
}

View File

@ -55,7 +55,7 @@ namespace IW4MAdmin.Application.Misc
var decryptedContent = new byte[encryptedContent.Length];
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(KeyLength));
var encryption = new AesGcm(keyGen.GetBytes(KeyLength),TagLength);
try
{

View File

@ -1,150 +1,255 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SharedLibraryCore;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using System;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Data.Models;
using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.GameEvent;
namespace IW4MAdmin.Application.Misc
namespace IW4MAdmin.Application.Misc;
public class IPAddressConverter : JsonConverter<IPAddress>
{
class IPAddressConverter : JsonConverter
public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPAddress));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return IPAddress.Parse((string)reader.Value);
}
var ipAddressString = reader.GetString();
return IPAddress.Parse(ipAddressString);
}
class IPEndPointConverter : JsonConverter
public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options)
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPEndPoint));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
IPEndPoint ep = (IPEndPoint)value;
JObject jo = new JObject();
jo.Add("Address", JToken.FromObject(ep.Address, serializer));
jo.Add("Port", ep.Port);
jo.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
IPAddress address = jo["Address"].ToObject<IPAddress>(serializer);
int port = (int)jo["Port"];
return new IPEndPoint(address, port);
}
}
class ClientEntityConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(EFClient);
public override object ReadJson(JsonReader reader, Type objectType,object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
{
return null;
}
var jsonObject = JObject.Load(reader);
return new EFClient
{
NetworkId = (long)jsonObject["NetworkId"],
ClientNumber = (int)jsonObject["ClientNumber"],
State = Enum.Parse<ClientState>(jsonObject["state"].ToString()),
CurrentAlias = new EFAlias()
{
IPAddress = (int?)jsonObject["IPAddress"],
Name = jsonObject["Name"].ToString()
}
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var client = value as EFClient;
var jsonObject = new JObject
{
{ "NetworkId", client.NetworkId },
{ "ClientNumber", client.ClientNumber },
{ "IPAddress", client.CurrentAlias?.IPAddress },
{ "Name", client.CurrentAlias?.Name },
{ "State", (int)client.State }
};
jsonObject.WriteTo(writer);
}
}
class GameEventConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>objectType == typeof(GameEvent);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
return new GameEvent
{
Type = Enum.Parse<EventType>(jsonObject["Type"].ToString()),
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 = value as GameEvent;
var jsonObject = new JObject
{
{ "Type", (int)gameEvent.Type },
{ "Subtype", gameEvent.Subtype },
{ "Source", (int)gameEvent.Source },
{ "RequiredEntity", (int)gameEvent.RequiredEntity },
{ "Data", gameEvent.Data },
{ "Message", gameEvent.Message },
{ "GameTime", gameEvent.GameTime },
{ "Origin", gameEvent.Origin != null ? JToken.FromObject(gameEvent.Origin, serializer) : null },
{ "Target", gameEvent.Target != null ? JToken.FromObject(gameEvent.Target, serializer) : null },
{ "ImpersonationOrigin", gameEvent.ImpersonationOrigin != null ? JToken.FromObject(gameEvent.ImpersonationOrigin, serializer) : null},
{ "IsRemote", gameEvent.IsRemote },
{ "Extra", gameEvent.Extra?.ToString() },
{ "Time", gameEvent.Time },
{ "IsBlocking", gameEvent.IsBlocking }
};
jsonObject.WriteTo(writer);
}
writer.WriteStringValue(value.ToString());
}
}
public class IPEndPointConverter : JsonConverter<IPEndPoint>
{
public override IPEndPoint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
IPAddress address = null;
var port = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
var propertyName = reader.GetString();
reader.Read();
switch (propertyName)
{
case "Address":
var addressString = reader.GetString();
address = IPAddress.Parse(addressString);
break;
case "Port":
port = reader.GetInt32();
break;
}
}
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
}
return new IPEndPoint(address, port);
}
public override void Write(Utf8JsonWriter writer, IPEndPoint value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("Address", value.Address.ToString());
writer.WriteNumber("Port", value.Port);
writer.WriteEndObject();
}
}
public class ClientEntityConverter : JsonConverter<EFClient>
{
public override EFClient Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
long networkId = default;
int clientNumber = default;
ClientState state = default;
var currentAlias = new EFAlias();
int? ipAddress = null;
string name = null;
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
var propertyName = reader.GetString();
reader.Read(); // Advance to the value.
switch (propertyName)
{
case "NetworkId":
networkId = reader.GetInt64();
break;
case "ClientNumber":
clientNumber = reader.GetInt32();
break;
case "State":
state = (ClientState)reader.GetInt32();
break;
case "IPAddress":
ipAddress = reader.TokenType != JsonTokenType.Null ? reader.GetInt32() : null;
break;
case "Name":
name = reader.GetString();
break;
}
}
}
currentAlias.IPAddress = ipAddress;
currentAlias.Name = name;
return new EFClient
{
NetworkId = networkId,
ClientNumber = clientNumber,
State = state,
CurrentAlias = currentAlias
};
}
public override void Write(Utf8JsonWriter writer, EFClient value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber("NetworkId", value.NetworkId);
writer.WriteNumber("ClientNumber", value.ClientNumber);
writer.WriteString("State", value.State.ToString());
if (value.CurrentAlias != null)
{
writer.WriteNumber("IPAddress", value.CurrentAlias.IPAddress ?? 0);
writer.WriteString("Name", value.CurrentAlias.Name);
}
else
{
writer.WriteNull("IPAddress");
writer.WriteNull("Name");
}
writer.WriteEndObject();
}
}
public class GameEventConverter : JsonConverter<GameEvent>
{
public override GameEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
var gameEvent = new GameEvent();
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
var propertyName = reader.GetString();
reader.Read();
switch (propertyName)
{
case "Type":
gameEvent.Type = (EventType)reader.GetInt32();
break;
case "Subtype":
gameEvent.Subtype = reader.GetString();
break;
case "Source":
gameEvent.Source = (EventSource)reader.GetInt32();
break;
case "RequiredEntity":
gameEvent.RequiredEntity = (EventRequiredEntity)reader.GetInt32();
break;
case "Data":
gameEvent.Data = reader.GetString();
break;
case "Message":
gameEvent.Message = reader.GetString();
break;
case "GameTime":
gameEvent.GameTime = reader.TokenType != JsonTokenType.Null ? reader.GetInt32() : null;
break;
case "Origin":
gameEvent.Origin = JsonSerializer.Deserialize<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
{
_pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
.GetPluginSubscription(_appConfig.Id, _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription
.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
@ -185,7 +185,7 @@ namespace IW4MAdmin.Application.Plugin
try
{
_pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
.GetPluginSubscription(_appConfig.Id, _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription
.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());

View File

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

View File

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

View File

@ -91,9 +91,9 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
? iqGroupedClientAliases.OrderByDescending(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)
.Take(query.Count);
.Take(query.Count).ToListAsync(); // todo: this change was for a pomelo limitation and may be addressed in future version
// this pulls in more records than we need, but it's more efficient than ordering grouped entities
var clientLookups = await clientAliases

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
// ReSharper disable CompareOfFloatsByEqualityOperator
#pragma warning disable CS0659
namespace Data.Models
{
public class Vector3
public class Vector3 : IParsable<Vector3>
{
[Key] public int Vector3Id { get; set; }
public float X { get; protected set; }
@ -78,7 +79,7 @@ namespace Data.Models
return Math.Sqrt((dx * dx) + (dy * dy));
}
public static double ViewAngleDistance(Vector3 a, Vector3 b, Vector3 c)
{
double dabX = Math.Abs(a.X - b.X);
@ -111,5 +112,30 @@ namespace Data.Models
public double Magnitude() => Math.Sqrt((X * X) + (Y * Y) + (Z * Z));
public double AngleBetween(Vector3 a) => Math.Acos(this.DotProduct(a) / (a.Magnitude() * this.Magnitude()));
public static Vector3 Parse(string s, IFormatProvider provider)
{
return Parse(s);
}
public static bool TryParse(string s, IFormatProvider provider, out Vector3 result)
{
result = new Vector3();
try
{
var parsed = Parse(s);
result.X = parsed.X;
result.Y = parsed.Y;
result.Z = parsed.Z;
return true;
}
catch
{
// ignored
}
return false;
}
}
}

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<LangVersion>Latest</LangVersion>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -10,11 +10,7 @@
<ItemGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

@ -3,7 +3,6 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using IW4MAdmin.Plugins.LiveRadar.Configuration;
using Microsoft.AspNetCore.Http;
@ -82,7 +81,7 @@ namespace IW4MAdmin.Plugins.LiveRadar.Web.Controllers
}
var radarInfo = server.GetClientsAsList()
.Select(client => client.GetAdditionalProperty<RadarEvent>("LiveRadar")).ToList();
.Select(client => client.GetAdditionalProperty<RadarDto>("LiveRadar")).ToList();
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">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
<PreserveCompilationContext Condition="'$(CONFIG)'!='Debug'">false</PreserveCompilationContext>
<PreserveCompilationContext>false</PreserveCompilationContext>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Version>0.1.0.0</Version>
@ -16,11 +16,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
@ -19,11 +19,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>MrAmos123</Authors>
@ -12,10 +12,7 @@
</PropertyGroup>
<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>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

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

View File

@ -21,15 +21,14 @@ public class Plugin : IPluginV2
public const string MuteKey = "IW4MMute";
public static IManager Manager { get; private set; } = null!;
public static Server.Game[] SupportedGames { get; private set; } = Array.Empty<Server.Game>();
private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"};
public static Server.Game[] SupportedGames { get; private set; } = [];
private static readonly string[] DisabledCommands = [nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"];
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private readonly MuteManager _muteManager;
private const string MuteInteraction = "Webfront::Profile::Mute";
public Plugin(IInteractionRegistration interactionRegistration,
IRemoteCommandService remoteCommandService, MuteManager muteManager)
public Plugin(IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService, MuteManager muteManager)
{
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
@ -72,7 +71,7 @@ public class Plugin : IPluginV2
return !DisabledCommands.Contains(command.GetType().Name) && !command.IsBroadcast;
});
_interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, token) =>
_interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, _) =>
{
if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value))
{
@ -80,16 +79,16 @@ public class Plugin : IPluginV2
}
var clientMuteMetaState =
(await _muteManager.GetCurrentMuteState(new EFClient {ClientId = targetClientId.Value}))
(await _muteManager.GetCurrentMuteState(new EFClient { ClientId = targetClientId.Value }))
.MuteState;
var server = manager.GetServers().First();
string GetCommandName(Type commandType) =>
manager.Commands.FirstOrDefault(command => command.GetType() == commandType)?.Name ?? "";
return clientMuteMetaState is MuteState.Unmuted or MuteState.Unmuting
? CreateMuteInteraction(targetClientId.Value, server, GetCommandName)
: CreateUnmuteInteraction(targetClientId.Value, server, GetCommandName);
string GetCommandName(Type commandType) =>
manager.Commands.FirstOrDefault(command => command.GetType() == commandType)?.Name ?? string.Empty;
});
return Task.CompletedTask;
}
@ -108,9 +107,9 @@ public class Plugin : IPluginV2
}
var networkIds = updateEvent.Clients.Select(client => client.NetworkId).ToList();
var ingameClients = updateEvent.Server.ConnectedClients.Where(client => networkIds.Contains(client.NetworkId));
var inGameClients = updateEvent.Server.ConnectedClients.Where(client => networkIds.Contains(client.NetworkId));
await Task.WhenAll(ingameClients.Select(async client =>
await Task.WhenAll(inGameClients.Select(async client =>
{
var muteMetaUpdate = await _muteManager.GetCurrentMuteState(client);
if (!muteMetaUpdate.CommandExecuted)
@ -136,7 +135,7 @@ public class Plugin : IPluginV2
{
var muteMetaSay = await _muteManager.GetCurrentMuteState(messageEvent.Origin);
if (muteMetaSay.MuteState == MuteState.Muted)
if (muteMetaSay.MuteState is MuteState.Muted)
{
// Let the client know when their mute expires.
messageEvent.Origin.Tell(Utilities.CurrentLocalization
@ -159,16 +158,16 @@ public class Plugin : IPluginV2
switch (muteMetaJoin)
{
case {MuteState: MuteState.Muted}:
case { MuteState: MuteState.Muted }:
// Let the client know when their mute expires.
state.Client.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt(
muteMetaJoin is {Expiration: not null}
muteMetaJoin is { Expiration: not null }
? muteMetaJoin.Expiration.Value.HumanizeForCurrentCulture()
: Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"],
muteMetaJoin.Reason));
break;
case {MuteState: MuteState.Unmuting}:
case { MuteState: MuteState.Unmuting }:
// Handle unmute of unmuted players.
await _muteManager.Unmute(state.Client.CurrentServer, Utilities.IW4MAdminClient(), state.Client,
muteMetaJoin.Reason ?? string.Empty);
@ -190,6 +189,29 @@ public class Plugin : IPluginV2
Values = (Dictionary<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
{
Name = "Duration",
@ -197,16 +219,16 @@ public class Plugin : IPluginV2
Type = "select",
Values = (Dictionary<string, string>?)new Dictionary<string, string>
{
{"5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture()},
{"30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture()},
{"1h", TimeSpan.FromHours(1).HumanizeForCurrentCulture()},
{"6h", TimeSpan.FromHours(6).HumanizeForCurrentCulture()},
{"1d", TimeSpan.FromDays(1).HumanizeForCurrentCulture()},
{"p", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_SELECTION_PERMANENT"]}
{ "5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture() },
{ "30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture() },
{ "1h", TimeSpan.FromHours(1).HumanizeForCurrentCulture() },
{ "6h", TimeSpan.FromHours(6).HumanizeForCurrentCulture() },
{ "1d", TimeSpan.FromDays(1).HumanizeForCurrentCulture() },
{ "p", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_SELECTION_PERMANENT"] }
}
};
var inputs = new[] {reasonInput, durationInput};
var inputs = new[] { reasonInput, presetReasonInput, durationInput };
var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData
@ -215,10 +237,10 @@ public class Plugin : IPluginV2
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"],
DisplayMeta = "oi-volume-off",
ActionPath = "DynamicAction",
ActionMeta = new()
ActionMeta = new Dictionary<string, string>
{
{"InteractionId", MuteInteraction},
{"Inputs", inputsJson},
{ "InteractionId", MuteInteraction },
{ "Inputs", inputsJson },
{
"ActionButtonLabel",
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"]
@ -227,7 +249,7 @@ public class Plugin : IPluginV2
"Name",
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"]
},
{"ShouldRefresh", true.ToString()}
{ "ShouldRefresh", true.ToString() }
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name,
@ -249,11 +271,14 @@ public class Plugin : IPluginV2
args.Add(duration);
}
if (meta.TryGetValue(reasonInput.Name, out var reason))
var definedReason = meta.TryGetValue(reasonInput.Name, out var reason) ? reason : string.Empty;
if (meta.TryGetValue(presetReasonInput.Name, out var presetReason) && string.IsNullOrWhiteSpace(definedReason))
{
args.Add(reason);
definedReason = presetReason;
}
args.Add(definedReason);
var commandResponse =
await _remoteCommandService.Execute(originId, targetId, muteCommand, args, server);
return string.Join(".", commandResponse.Select(result => result.Response));
@ -271,21 +296,20 @@ public class Plugin : IPluginV2
Type = "text",
};
var inputs = new[] {reasonInput};
var inputs = new[] { reasonInput };
var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData
{
EntityId = targetClientId,
Name = Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
DisplayMeta = "oi-volume-high",
ActionPath = "DynamicAction",
ActionMeta = new()
ActionMeta = new Dictionary<string, string>
{
{"InteractionId", MuteInteraction},
{"Outputs", reasonInput.Name},
{"Inputs", inputsJson},
{ "InteractionId", MuteInteraction },
{ "Outputs", reasonInput.Name },
{ "Inputs", inputsJson },
{
"ActionButtonLabel",
Utilities.CurrentLocalization.LocalizationIndex[
@ -296,7 +320,7 @@ public class Plugin : IPluginV2
Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"]
},
{"ShouldRefresh", true.ToString()}
{ "ShouldRefresh", true.ToString() }
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name,

View File

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

View File

@ -94,7 +94,7 @@ const plugin = {
onServerValueSetCompleted: async function (serverValueEvent) {
this.logger.logDebug('Set {dvarName}={dvarValue} success={success} from {server}', serverValueEvent.valueName,
serverValueEvent.value, serverValueEvent.success, serverValueEvent.server.id);
if (serverValueEvent.valueName !== inDvar && serverValueEvent.valueName !== outDvar) {
this.logger.logDebug('Ignoring set complete of {name}', serverValueEvent.valueName);
return;
@ -124,7 +124,7 @@ const plugin = {
// loop restarts
this.requestGetDvar(inDvar, serverValueEvent.server);
},
onServerMonitoringStart: function (monitorStartEvent) {
this.initializeServer(monitorStartEvent.server);
},
@ -162,7 +162,7 @@ const plugin = {
serverState.enabled = true;
serverState.running = true;
serverState.initializationInProgress = false;
// todo: this might not work for all games
responseEvent.server.rconParser.configuration.floodProtectInterval = 150;
@ -233,7 +233,7 @@ const plugin = {
// todo: refactor to mapping if possible
if (event.eventType === 'ClientDataRequested') {
const client = server.getClientByNumber(event.clientNumber);
const client = server.connectedClients[event.clientNumber];
if (client != null) {
this.logger.logDebug('Found client {name}', client.name);
@ -269,8 +269,9 @@ const plugin = {
}
}
let _;
if (event.eventType === 'SetClientDataRequested') {
let client = server.getClientByNumber(event.clientNumber);
let client = server.connectedClients[event.clientNumber];
let clientId;
if (client != null) {
@ -298,12 +299,12 @@ const plugin = {
const parsedValue = parseInt(event.data['value']);
const key = event.data['key'].toString();
if (!isNaN(parsedValue)) {
event.data['direction'] = 'up' ?
_ = event.data['direction'] === 'increment' ?
(await metaService.incrementPersistentMeta(key, parsedValue, clientId, token)).result :
(await metaService.decrementPersistentMeta(key, parsedValue, clientId, token)).result;
}
} else {
const _ = (await metaService.setPersistentMeta(event.data['key'], event.data['value'], clientId, token)).result;
_ = (await metaService.setPersistentMeta(event.data['key'], event.data['value'], clientId, token)).result;
}
if (event.data['key'] === 'PersistentClientGuid') {
@ -342,34 +343,34 @@ const plugin = {
if (typeof response !== 'string' && !(response instanceof String)) {
response = JSON.stringify(response);
}
const max = 10;
this.logger.logDebug(`response length ${response.length}`);
let quoteReplace = '\\"';
// todo: may be more than just T6
if (server.gameCode === 'T6') {
quoteReplace = '\\\\"';
}
let chunks = chunkString(response.replace(/"/gm, quoteReplace).replace(/[\n|\t]/gm, ''), 800);
if (chunks.length > max) {
this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`);
chunks = chunks.slice(0, max);
}
this.logger.logDebug(`chunk size ${chunks.length}`);
for (let i = 0; i < chunks.length; i++) {
this.sendEventMessage(server, false, 'UrlRequestCompleted', null, null,
null, { entity: event.data.entity, remaining: chunks.length - (i + 1), response: chunks[i]});
null, {entity: event.data.entity, remaining: chunks.length - (i + 1), response: chunks[i]});
}
});
}
if (event.eventType === 'RegisterCommandRequested') {
this.registerDynamicCommand(event);
}
if (event.eventType === 'GetBusModeRequested') {
if (event.data?.directory && event.data?.mode) {
busMode = event.data.mode;
@ -433,10 +434,10 @@ const plugin = {
});
}
});
return;
}
const serverEvents = importNamespace('SharedLibraryCore.Events.Server');
const requestEvent = new serverEvents.ServerValueRequestEvent(dvarName, server);
requestEvent.delayMs = this.config.pollingRate;
@ -467,8 +468,8 @@ const plugin = {
requestSetDvar: function (dvarName, dvarValue, server) {
const serverState = servers[server.id];
if ( busMode === 'file' ) {
if (busMode === 'file') {
this.scriptHelper.requestNotifyAfterDelay(250, async () => {
const io = importNamespace('System.IO');
try {
@ -493,7 +494,7 @@ const plugin = {
});
}
})
return;
}
@ -526,7 +527,7 @@ const plugin = {
}
},
parseUrlRequest: function(event) {
parseUrlRequest: function (event) {
const url = event.data?.url;
if (url === undefined) {
@ -556,8 +557,8 @@ const plugin = {
const script = importNamespace('IW4MAdmin.Application.Plugin.Script');
return new script.ScriptPluginWebRequest(url, body, method, contentType, headerDict);
},
registerDynamicCommand: function(event) {
registerDynamicCommand: function (event) {
const commandWrapper = {
commands: [{
name: event.data['name'] || 'DEFAULT',
@ -571,9 +572,9 @@ const plugin = {
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
return;
}
if (gameEvent.data === '--reload' && gameEvent.origin.level === 'Owner') {
this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, { name: gameEvent.extra.name });
this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, {name: gameEvent.extra.name});
} else {
sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, {
args: gameEvent.data
@ -582,7 +583,7 @@ const plugin = {
}
}]
}
this.scriptHelper.registerDynamicCommand(commandWrapper);
}
};
@ -920,6 +921,6 @@ const fileForDvar = (dvar) => {
if (dvar === inDvar) {
return busFileIn;
}
return busFileOut;
}

View File

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

View File

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

View File

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

View File

@ -6,8 +6,8 @@ using SharedLibraryCore.Database.Models;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using Newtonsoft.Json.Linq;
using Humanizer;
using Data.Abstractions;
using Data.Models;
@ -108,8 +108,9 @@ public class Plugin : IPluginV2
var response =
await wc.GetStringAsync(new Uri(
$"http://ip-api.com/json/{ip}?lang={Utilities.CurrentLocalization.LocalizationName.Split("-").First().ToLower()}"));
var responseObj = JObject.Parse(response);
response = responseObj["country"]?.ToString();
var json = JsonDocument.Parse(response);
response = json.RootElement.TryGetProperty("country", out var countryElement) ? countryElement.GetString() : null;
return string.IsNullOrEmpty(response)
? Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_UNKNOWN_COUNTRY"]

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Welcome</PackageId>
@ -15,12 +15,8 @@
<LangVersion>Latest</LangVersion>
</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>
<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>
</Project>

View File

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

View File

@ -1,41 +1,40 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Text.Json.Serialization;
using SharedLibraryCore.Helpers;
using static Data.Models.Client.EFClient;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Configuration
namespace SharedLibraryCore.Configuration;
/// <summary>
/// Config driven command properties
/// </summary>
public class CommandProperties
{
/// <summary>
/// Config driven command properties
/// Specifies the command name
/// </summary>
public class CommandProperties
{
/// <summary>
/// Specifies the command name
/// </summary>
public string Name { get; set; }
public string Name { get; set; }
/// <summary>
/// Alias of this command
/// </summary>
public string Alias { get; set; }
/// <summary>
/// Alias of this command
/// </summary>
public string Alias { get; set; }
/// <summary>
/// Specifies the minimum permission level needed to execute the
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public Permission MinimumPermission { get; set; }
/// <summary>
/// Specifies the minimum permission level needed to execute the
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public Permission MinimumPermission { get; set; }
/// <summary>
/// Indicates if the command can be run by another user (impersonation)
/// </summary>
public bool AllowImpersonation { get; set; }
/// <summary>
/// Indicates if the command can be run by another user (impersonation)
/// </summary>
public bool AllowImpersonation { get; set; }
/// <summary>
/// Specifies the games supporting the functionality of the command
/// </summary>
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public Game[] SupportedGames { get; set; } = Array.Empty<Game>();
}
/// <summary>
/// Specifies the games supporting the functionality of the command
/// </summary>
[JsonConverter(typeof(GameArrayJsonConverter))]
public Game[] SupportedGames { get; set; } = Array.Empty<Game>();
}

View File

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

View File

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

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.Client;
using SharedLibraryCore.Database.Models;
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 Newtonsoft.Json;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SharedLibraryCore.Helpers
namespace SharedLibraryCore.Helpers;
/// <summary>
/// JSON converter for the build number
/// </summary>
public class BuildNumberJsonConverter : JsonConverter<BuildNumber>
{
/// <summary>
/// JSON converter for the build number
/// </summary>
public class BuildNumberJsonConverter : JsonConverter
public override BuildNumber Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
return BuildNumber.Parse(reader.Value.ToString());
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
var stringValue = reader.GetString();
return BuildNumber.Parse(stringValue);
}
}
public override void Write(Utf8JsonWriter writer, BuildNumber value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
public class GameArrayJsonConverter : JsonConverter<Server.Game[]>
{
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>
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)
{
return coreEvent switch
@ -100,6 +105,7 @@ public interface IGameEventSubscriptions
ClientCommandEvent clientCommandEvent => ClientEnteredCommand?.InvokeAsync(clientCommandEvent, token) ?? Task.CompletedTask,
ClientMessageEvent clientMessageEvent => ClientMessaged?.InvokeAsync(clientMessageEvent, token) ?? Task.CompletedTask,
GameScriptEvent gameScriptEvent => ScriptEventTriggered?.InvokeAsync(gameScriptEvent, token) ?? Task.CompletedTask,
GameLogEvent gameLogEvent => GameLogEventTriggered?.InvokeAsync(gameLogEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask
};
}
@ -116,5 +122,6 @@ public interface IGameEventSubscriptions
ClientMessaged = null;
ClientEnteredCommand = null;
ScriptEventTriggered = null;
GameLogEventTriggered = null;
}
}

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>
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)
{
return coreEvent switch
@ -88,6 +93,7 @@ public interface IGameServerEventSubscriptions
ServerValueReceiveEvent serverValueReceiveEvent => ServerValueReceived?.InvokeAsync(serverValueReceiveEvent, token) ?? Task.CompletedTask,
ServerValueSetRequestEvent serverValueSetRequestEvent => ServerValueSetRequested?.InvokeAsync(serverValueSetRequestEvent, token) ?? Task.CompletedTask,
ServerValueSetCompleteEvent serverValueSetCompleteEvent => ServerValueSetCompleted?.InvokeAsync(serverValueSetCompleteEvent, token) ?? Task.CompletedTask,
ServerStatusReceiveEvent serverStatusReceiveEvent => ServerStatusReceived?.InvokeAsync(serverStatusReceiveEvent, token) ?? Task.CompletedTask,
_ => Task.CompletedTask
};
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
@ -19,6 +20,9 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
IPEndPoint ResolvedIpEndPoint { get; }
IRConParser RconParser { get; }
/// <summary>
/// Execute a server command
/// </summary>
@ -35,72 +39,72 @@ namespace SharedLibraryCore.Interfaces
/// <param name="token"><see cref="CancellationToken"/></param>
/// <returns></returns>
Task SetDvarAsync(string name, object value, CancellationToken token = default);
/// <summary>
/// Time the most recent match ended
/// </summary>
DateTime? MatchEndTime { get; }
/// <summary>
/// Time the current match started
/// </summary>
DateTime? MatchStartTime { get; }
/// <summary>
/// List of connected clients
/// </summary>
IReadOnlyList<EFClient> ConnectedClients { get; }
/// <summary>
/// Game code corresponding to the development studio project
/// </summary>
Reference.Game GameCode { get; }
/// <summary>
/// Indicates if the anticheat/custom callbacks/live radar integration is enabled
/// </summary>
bool IsLegacyGameIntegrationEnabled { get; }
/// <summary>
/// Unique identifier for the server (typically ip:port)
/// </summary>
string Id { get; }
/// <summary>
/// Network address the server is listening on
/// </summary>
string ListenAddress { get; }
/// <summary>
/// Network port the server is listening on
/// </summary>
int ListenPort { get; }
/// <summary>
/// Name of the server (hostname)
/// </summary>
string ServerName { get; }
/// <summary>
/// Current gametype
/// </summary>
string Gametype { get; }
/// <summary>
/// Game password (required to join)
/// </summary>
string GamePassword { get; }
/// <summary>
/// Number of private client slots
/// </summary>
int PrivateClientSlots { get; }
/// <summary>
/// Current map the game server is running
/// </summary>
Map Map { get; }
/// <summary>
/// Database id for EFServer table and references
/// </summary>

View File

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

View File

@ -15,35 +15,35 @@ namespace SharedLibraryCore.Interfaces
/// <summary>
/// stores the game/client specific version (usually the value of the "version" DVAR)
/// </summary>
string Version { get; }
string Version { get; set; }
/// <summary>
/// specifies the game name (usually the internal studio iteration ie: IW4, T5 etc...)
/// </summary>
Game GameName { get; }
Game GameName { get; set; }
/// <summary>
/// indicates if the game supports generating a log path from DVAR retrieval
/// of fs_game, fs_basepath, g_log
/// </summary>
bool CanGenerateLogPath { get; }
bool CanGenerateLogPath { get; set; }
/// <summary>
/// specifies the name of the parser
/// </summary>
string Name { get; }
string Name { get; set; }
/// <summary>
/// specifies the type of rcon engine
/// eg: COD, Source
/// </summary>
string RConEngine { get; }
string RConEngine { get; set; }
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
bool IsOneLog { get; }
bool IsOneLog { get; set; }
/// <summary>
/// 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="token"></param>
/// <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>
/// set value of DVAR by name
@ -65,7 +66,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="token"></param>
/// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
/// <summary>
/// executes a console command on the server
/// </summary>

View File

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

View File

@ -31,5 +31,10 @@ namespace SharedLibraryCore.Interfaces
/// active clients
/// </summary>
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.Globalization;
using System.Text.Json.Serialization;
using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Localization
@ -8,6 +9,8 @@ namespace SharedLibraryCore.Localization
{
private string localizationName;
public Layout() { }
public Layout(Dictionary<string, string> set)
{
LocalizationIndex = new TranslationLookup
@ -27,7 +30,7 @@ namespace SharedLibraryCore.Localization
}
public TranslationLookup LocalizationIndex { get; set; }
public CultureInfo Culture { get; private set; }
[JsonIgnore] public CultureInfo Culture { get; private set; }
}
public class TranslationLookup : ITranslationLookup
@ -47,4 +50,4 @@ namespace SharedLibraryCore.Localization
}
}
}
}
}

View File

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

View File

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

View File

@ -2,9 +2,9 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2023.4.5.1</Version>
<Version>2024.01.01.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2023.4.5.1</PackageVersion>
<PackageVersion>2024.06.22.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
@ -34,37 +34,31 @@
</PropertyGroup>
<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.ru" 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.pt" Version="2.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj">
<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
<IncludeAssets>Data.dll</IncludeAssets>
</ProjectReference>
<ProjectReference Include="..\Data\Data.csproj">
<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
<IncludeAssets>Data.dll</IncludeAssets>
</ProjectReference>
</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>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>

View File

@ -868,7 +868,14 @@ namespace SharedLibraryCore
{
try
{
return await server.RconParser.GetStatusAsync(server.RemoteConnection, token);
var response = await server.RconParser.GetStatusAsync(server.RemoteConnection, token);
server.Manager.QueueEvent(new ServerStatusReceiveEvent
{
Response = response
});
return response;
}
catch (TaskCanceledException)
@ -1332,6 +1339,14 @@ namespace SharedLibraryCore
return serviceCollection;
}
public static TimeSpan GetExponentialBackoffDelay(int retryCount, int staticDelay = 5)
{
var maxTimeout = TimeSpan.FromMinutes(2.1);
const double factor = 2.0;
var delay = Math.Min(staticDelay + Math.Pow(factor, retryCount - 1), maxTimeout.TotalSeconds);
return TimeSpan.FromSeconds(delay);
}
public static void ExecuteAfterDelay(TimeSpan duration, Func<CancellationToken, Task> action, CancellationToken token = default) =>
ExecuteAfterDelay((int)duration.TotalMilliseconds, action, token);

View File

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

View File

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

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">
<link rel="stylesheet" href="~/lib/halfmoon/css/halfmoon-variables.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 include="Production">
<link rel="stylesheet" href="~/css/global.min.css?version=@ViewBag.Version"/>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
@ -33,22 +33,18 @@
<ItemGroup>
<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="Views\*.*" CopyToPublishDirectory="Never" />
<None Include="wwwroot\css\global.min.css" CopyToPublishDirectory="PreserveNewest" />
<None Include="wwwroot\js\global.min.js" CopyToPublishDirectory="PreserveNewest" />
<None Include="wwwroot\images\**\*.*" CopyToPublishDirectory="PreserveNewest" />
<Content Remove="wwwroot\css\src\main.css.map" />
<Content Remove="dotnet-bundle.runtimeconfig.json" />
<Content Remove="dotnet-bundle.deps.json" />
<None Remove="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildWebCompiler2022" Version="1.14.10" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
<PackageReference Include="Microsoft.AspNetCore.ConcurrencyLimiter" Version="6.0.16" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.8" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Microsoft.AspNetCore.ConcurrencyLimiter" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.6" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
</ItemGroup>
@ -78,7 +74,4 @@
</VisualStudio>
</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>

View File

@ -3,9 +3,9 @@
"outputFileName": "wwwroot/css/global.min.css",
"inputFiles": [
"wwwroot/lib/halfmoon/css/halfmoon-variables.min.css",
"wwwroot/css/global.css",
"wwwroot/lib/chart.js/dist/Chart.min.css",
"wwwroot/css/open-iconic.css"
"wwwroot/css/open-iconic-bootstrap-override.css",
"wwwroot/css/main.css"
]
},
{

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