From 9c94ee26e5485ef5d2be39817742b6c8344adcb6 Mon Sep 17 00:00:00 2001 From: Ilja Jusupov Date: Mon, 13 Jun 2022 00:05:36 +0300 Subject: [PATCH] Update --- .gitignore | 402 +----- AcPlugins/AcServerPlugin.cs | 130 ++ AcPlugins/AcServerPluginManager.cs | 1187 +++++++++++++++++ AcPlugins/AcServerPluginManagerSettings.cs | 43 + AcPlugins/CspCommands/CommandHandshakeIn.cs | 14 + AcPlugins/CspCommands/CommandHandshakeOut.cs | 17 + AcPlugins/CspCommands/CommandWeatherSetV1.cs | 39 + AcPlugins/CspCommands/CommandWeatherSetV2.cs | 29 + AcPlugins/CspCommands/CommandWeatherType.cs | 37 + AcPlugins/CspCommands/CspCommandsUtils.cs | 24 + AcPlugins/CspCommands/ICspCommand.cs | 5 + AcPlugins/ExternalPluginInfo.cs | 13 + AcPlugins/Helpers/AcMessageParser.cs | 98 ++ AcPlugins/Helpers/DuplexUdpClient.cs | 169 +++ AcPlugins/Helpers/ISessionReportHandler.cs | 7 + AcPlugins/Helpers/TimestampedBytes.cs | 18 + AcPlugins/Helpers/Vector3F.cs | 56 + AcPlugins/Info/DriverInfo.cs | 237 ++++ AcPlugins/Info/IncidentInfo.cs | 28 + AcPlugins/Info/LapInfo.cs | 30 + AcPlugins/Info/SessionInfo.cs | 81 ++ AcPlugins/Kunos/ACSProtocol.cs | 71 + AcPlugins/Messages/MsgCarInfo.cs | 37 + AcPlugins/Messages/MsgCarUpdate.cs | 37 + AcPlugins/Messages/MsgChat.cs | 33 + AcPlugins/Messages/MsgClientEvent.cs | 49 + AcPlugins/Messages/MsgClientLoaded.cs | 21 + AcPlugins/Messages/MsgConnectionClosed.cs | 31 + AcPlugins/Messages/MsgError.cs | 19 + AcPlugins/Messages/MsgLapCompleted.cs | 48 + .../MsgLapCompletedLeaderboardEnty.cs | 26 + AcPlugins/Messages/MsgNewConnection.cs | 31 + AcPlugins/Messages/MsgNewSession.cs | 9 + AcPlugins/Messages/MsgSessionEnded.cs | 20 + AcPlugins/Messages/MsgSessionInfo.cs | 108 ++ AcPlugins/Messages/MsgVersionInfo.cs | 21 + AcPlugins/Messages/RequestAdminCommand.cs | 19 + AcPlugins/Messages/RequestBroadcastChat.cs | 19 + AcPlugins/Messages/RequestCarInfo.cs | 19 + AcPlugins/Messages/RequestKickUser.cs | 19 + AcPlugins/Messages/RequestNextSession.cs | 13 + AcPlugins/Messages/RequestRealtimeInfo.cs | 20 + AcPlugins/Messages/RequestRestartSession.cs | 13 + AcPlugins/Messages/RequestSendChat.cs | 22 + AcPlugins/Messages/RequestSessionInfo.cs | 23 + AcPlugins/Messages/RequestSetSession.cs | 54 + AcPlugins/PluginMessage.cs | 94 ++ AcTools.ServerPlugin.DynamicConditions.csproj | 123 ++ AcTools.ServerPlugin.DynamicConditions.sln | 16 + Data/CommonAcConsts.cs | 13 + Data/Game.cs | 36 + Data/WeatherDescription.cs | 33 + Data/WeatherType.cs | 38 + LiveConditionsServerPlugin.cs | 532 ++++++++ OpenWeatherApiProvider.cs | 270 ++++ Program.cs | 61 + ProgramParams.cs | 79 ++ Properties/AssemblyInfo.cs | 39 + README.md | 48 +- Utils/DateTimeExtension.cs | 9 + Utils/Half.cs | 1155 ++++++++++++++++ Utils/Logging.cs | 20 + Utils/MainExecutingFile.cs | 13 + Utils/MathUtils.cs | 111 ++ Utils/TaskExtension.cs | 11 + packages.config | 5 + 66 files changed, 5752 insertions(+), 400 deletions(-) create mode 100644 AcPlugins/AcServerPlugin.cs create mode 100644 AcPlugins/AcServerPluginManager.cs create mode 100644 AcPlugins/AcServerPluginManagerSettings.cs create mode 100644 AcPlugins/CspCommands/CommandHandshakeIn.cs create mode 100644 AcPlugins/CspCommands/CommandHandshakeOut.cs create mode 100644 AcPlugins/CspCommands/CommandWeatherSetV1.cs create mode 100644 AcPlugins/CspCommands/CommandWeatherSetV2.cs create mode 100644 AcPlugins/CspCommands/CommandWeatherType.cs create mode 100644 AcPlugins/CspCommands/CspCommandsUtils.cs create mode 100644 AcPlugins/CspCommands/ICspCommand.cs create mode 100644 AcPlugins/ExternalPluginInfo.cs create mode 100644 AcPlugins/Helpers/AcMessageParser.cs create mode 100644 AcPlugins/Helpers/DuplexUdpClient.cs create mode 100644 AcPlugins/Helpers/ISessionReportHandler.cs create mode 100644 AcPlugins/Helpers/TimestampedBytes.cs create mode 100644 AcPlugins/Helpers/Vector3F.cs create mode 100644 AcPlugins/Info/DriverInfo.cs create mode 100644 AcPlugins/Info/IncidentInfo.cs create mode 100644 AcPlugins/Info/LapInfo.cs create mode 100644 AcPlugins/Info/SessionInfo.cs create mode 100644 AcPlugins/Kunos/ACSProtocol.cs create mode 100644 AcPlugins/Messages/MsgCarInfo.cs create mode 100644 AcPlugins/Messages/MsgCarUpdate.cs create mode 100644 AcPlugins/Messages/MsgChat.cs create mode 100644 AcPlugins/Messages/MsgClientEvent.cs create mode 100644 AcPlugins/Messages/MsgClientLoaded.cs create mode 100644 AcPlugins/Messages/MsgConnectionClosed.cs create mode 100644 AcPlugins/Messages/MsgError.cs create mode 100644 AcPlugins/Messages/MsgLapCompleted.cs create mode 100644 AcPlugins/Messages/MsgLapCompletedLeaderboardEnty.cs create mode 100644 AcPlugins/Messages/MsgNewConnection.cs create mode 100644 AcPlugins/Messages/MsgNewSession.cs create mode 100644 AcPlugins/Messages/MsgSessionEnded.cs create mode 100644 AcPlugins/Messages/MsgSessionInfo.cs create mode 100644 AcPlugins/Messages/MsgVersionInfo.cs create mode 100644 AcPlugins/Messages/RequestAdminCommand.cs create mode 100644 AcPlugins/Messages/RequestBroadcastChat.cs create mode 100644 AcPlugins/Messages/RequestCarInfo.cs create mode 100644 AcPlugins/Messages/RequestKickUser.cs create mode 100644 AcPlugins/Messages/RequestNextSession.cs create mode 100644 AcPlugins/Messages/RequestRealtimeInfo.cs create mode 100644 AcPlugins/Messages/RequestRestartSession.cs create mode 100644 AcPlugins/Messages/RequestSendChat.cs create mode 100644 AcPlugins/Messages/RequestSessionInfo.cs create mode 100644 AcPlugins/Messages/RequestSetSession.cs create mode 100644 AcPlugins/PluginMessage.cs create mode 100644 AcTools.ServerPlugin.DynamicConditions.csproj create mode 100644 AcTools.ServerPlugin.DynamicConditions.sln create mode 100644 Data/CommonAcConsts.cs create mode 100644 Data/Game.cs create mode 100644 Data/WeatherDescription.cs create mode 100644 Data/WeatherType.cs create mode 100644 LiveConditionsServerPlugin.cs create mode 100644 OpenWeatherApiProvider.cs create mode 100644 Program.cs create mode 100644 ProgramParams.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Utils/DateTimeExtension.cs create mode 100644 Utils/Half.cs create mode 100644 Utils/Logging.cs create mode 100644 Utils/MainExecutingFile.cs create mode 100644 Utils/MathUtils.cs create mode 100644 Utils/TaskExtension.cs create mode 100644 packages.config diff --git a/.gitignore b/.gitignore index 426d76d..544eb45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,398 +1,4 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml +.idea +bin +obj +packages \ No newline at end of file diff --git a/AcPlugins/AcServerPlugin.cs b/AcPlugins/AcServerPlugin.cs new file mode 100644 index 0000000..54e47c1 --- /dev/null +++ b/AcPlugins/AcServerPlugin.cs @@ -0,0 +1,130 @@ +using System; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins { + public interface IAcServerPlugin : IDisposable { + string GetName(); + + void OnInit(AcServerPluginManager manager); + + void OnConnected(); + + void OnDisconnected(); + + /// + /// Handler for commands. + /// + /// Returns true if command was handled. + bool OnCommandEntered(string cmd); + + #region Handlers for raw acServer messages + void OnSessionInfo(MsgSessionInfo msg); + + void OnNewSession(MsgSessionInfo msg); + + void OnSessionEnded(MsgSessionEnded msg); + + void OnNewConnection(MsgNewConnection msg); + + void OnConnectionClosed(MsgConnectionClosed msg); + + void OnCarInfo(MsgCarInfo msg); + + void OnCarUpdate(MsgCarUpdate msg); + + void OnCollision(MsgClientEvent msg); + + void OnLapCompleted(MsgLapCompleted msg); + + void OnClientLoaded(MsgClientLoaded msg); + + void OnChatMessage(MsgChat msg); + + void OnProtocolVersion(MsgVersionInfo msg); + + void OnServerError(MsgError msg); + #endregion Event handlers for raw acServer messages + + #region Handlers for events refined by PluginManager + /// + /// This is triggered after all realtime reports per interval have arrived - they are now + /// up-to-date and can be accessed via the DriverInfo mechanics + /// + void OnBulkCarUpdateFinished(); + + void OnLapCompleted(LapInfo msg); + + void OnCollision(IncidentInfo msg); + + void OnCarUpdate(DriverInfo driverInfo); + + void OnAcServerTimeout(); + + void OnAcServerAlive(); + #endregion Handlers for events refined by PluginManager + } + + public class AcServerPlugin : IAcServerPlugin { + protected AcServerPluginManager PluginManager { get; private set; } + + public virtual string GetName() { + return GetType().Name; + } + + void IAcServerPlugin.OnInit(AcServerPluginManager manager) { + PluginManager = manager; + OnInit(); + } + + public virtual void OnInit() { } + + public virtual void OnConnected() { } + + public virtual void OnDisconnected() { } + + public virtual bool OnCommandEntered(string cmd) { + return false; + } + + public virtual void OnSessionInfo(MsgSessionInfo msg) { } + + public virtual void OnNewSession(MsgSessionInfo msg) { } + + public virtual void OnSessionEnded(MsgSessionEnded msg) { } + + public virtual void OnNewConnection(MsgNewConnection msg) { } + + public virtual void OnConnectionClosed(MsgConnectionClosed msg) { } + + public virtual void OnCarInfo(MsgCarInfo msg) { } + + public virtual void OnCarUpdate(MsgCarUpdate msg) { } + + public virtual void OnCollision(MsgClientEvent msg) { } + + public virtual void OnLapCompleted(MsgLapCompleted msg) { } + + public virtual void OnClientLoaded(MsgClientLoaded msg) { } + + public virtual void OnChatMessage(MsgChat msg) { } + + public virtual void OnProtocolVersion(MsgVersionInfo msg) { } + + public virtual void OnServerError(MsgError msg) { } + + public virtual void OnBulkCarUpdateFinished() { } + + public virtual void OnLapCompleted(LapInfo msg) { } + + public virtual void OnCollision(IncidentInfo msg) { } + + public virtual void OnCarUpdate(DriverInfo driverInfo) { } + + public virtual void OnAcServerTimeout() { } + + public virtual void OnAcServerAlive() { } + + public virtual void Dispose() { } + } +} \ No newline at end of file diff --git a/AcPlugins/AcServerPluginManager.cs b/AcPlugins/AcServerPluginManager.cs new file mode 100644 index 0000000..3604fec --- /dev/null +++ b/AcPlugins/AcServerPluginManager.cs @@ -0,0 +1,1187 @@ +/* From https://github.com/minolin/acplugins */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Threading; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages; +using AcTools.ServerPlugin.DynamicConditions.Data; +using AcTools.ServerPlugin.DynamicConditions.Utils; +using JetBrains.Annotations; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins { + public sealed class AcServerPluginManager : IDisposable { + public const int RequiredProtocolVersion = 4; + + public int ProtocolVersion { get; private set; } + + /// + /// Gives the last timestamp where the acServer was active, used for KeepAlive + /// + public DateTime LastServerActivity { get; private set; } + + public SessionInfo CurrentSession => _currentSession; + + public SessionInfo PreviousSession => _previousSession; + + public DriverInfo[] GetDriverInfos() { + lock (_lockObject) { + return _currentSession.Drivers.ToArray(); + } + } + + public bool TryGetDriverInfo(byte carId, out DriverInfo driver) { + lock (_lockObject) { + return _carUsedByDictionary.TryGetValue(carId, out driver); + } + } + + public DriverInfo GetDriverInfo(byte carId) { + if (TryGetDriverInfo(carId, out var driver)) { + return driver; + } else { + return null; + } + } + + public DriverInfo GetDriverByConnectionId(int connectionId) { + return _currentSession.Drivers[connectionId]; + } + + #region Private stuff + [NotNull] + private readonly AcServerPluginManagerSettings _settings; + + private readonly DuplexUdpClient _udp; + private readonly List _plugins; + private readonly List _externalPlugins; + private readonly Dictionary _openExternalPlugins; + private readonly List _sessionReportHandlers = new List(); + private readonly object _lockObject = new object(); + + private readonly Dictionary _carUsedByDictionary = new Dictionary(); + private int _lastCarUpdateCarId = -1; + private SessionInfo _currentSession = new SessionInfo(); + private SessionInfo _previousSession = new SessionInfo(); + + private MsgSessionInfo _nextSessionStarting; + #endregion + + public AcServerPluginManager([NotNull] AcServerPluginManagerSettings settings) { + _settings = settings; + _currentSession.MaxClients = settings.Capacity; + + _plugins = new List(); + _udp = new DuplexUdpClient(); + _externalPlugins = new List(); + _openExternalPlugins = new Dictionary(); + + ProtocolVersion = -1; + } + + public int MaxClients() { + return _currentSession.MaxClients; + } + + public void AddInternalPlugin(string assemblyName, string typeName) { + var assembly = Assembly.Load(assemblyName); + var type = assembly.GetType(typeName); + var plugin = (IAcServerPlugin)Activator.CreateInstance(type); + AddPlugin(plugin); + } + + public void AddSessionReportHandler(ISessionReportHandler handler) { + _sessionReportHandlers.Add(handler); + } + + public void AddPlugin(IAcServerPlugin plugin) { + lock (_lockObject) { + if (plugin == null) { + throw new ArgumentNullException(nameof(plugin)); + } + + if (IsConnected) { + throw new Exception("Cannot add plugin while connected."); + } + + if (_plugins.Contains(plugin)) { + throw new Exception("Plugin was added before."); + } + + _plugins.Add(plugin); + + plugin.OnInit(this); + } + } + + [NotNull] + public T AddPlugin() where T : IAcServerPlugin, new() { + var ret = new T(); + AddPlugin(ret); + return ret; + } + + public void RemovePlugin(IAcServerPlugin plugin) { + lock (_lockObject) { + if (plugin == null) { + throw new ArgumentNullException(nameof(plugin)); + } + + if (IsConnected) { + throw new Exception("Cannot remove plugin while connected."); + } + + if (!_plugins.Contains(plugin)) { + throw new Exception("Plugin was not added before."); + } + + _plugins.Remove(plugin); + } + } + + public void AddExternalPlugin(ExternalPluginInfo externalPlugin) { + lock (_lockObject) { + if (externalPlugin == null) { + throw new ArgumentNullException(nameof(externalPlugin)); + } + + if (IsConnected) { + throw new Exception("Cannot add external plugin while connected."); + } + + if (_externalPlugins.Contains(externalPlugin)) { + throw new Exception("External plugin was added before."); + } + + _externalPlugins.Add(externalPlugin); + } + } + + public void RemoveExternalPlugin(ExternalPluginInfo externalPlugin) { + lock (_lockObject) { + if (externalPlugin == null) { + throw new ArgumentNullException(nameof(externalPlugin)); + } + + if (IsConnected) { + throw new Exception("Cannot remove external plugin while connected."); + } + + if (!_externalPlugins.Contains(externalPlugin)) { + throw new Exception("External plugin was not added before."); + } + + _externalPlugins.Remove(externalPlugin); + } + } + + private DriverInfo GetDriverReportForCarId(byte carId) { + if (!_carUsedByDictionary.TryGetValue(carId, out var driverReport)) { + // It seems we missed the OnNewConnection for this driver + driverReport = new DriverInfo { + ConnectionId = _currentSession.Drivers.Count, + ConnectedTimestamp = DateTime.UtcNow.Ticks, // Obviously not correct but better than nothing + CarId = carId + }; + + _currentSession.Drivers.Add(driverReport); + _carUsedByDictionary.Add(driverReport.CarId, driverReport); + RequestCarInfo(carId); + } else if (string.IsNullOrEmpty(driverReport.DriverGuid)) { + // It seems we did not yet receive carInfo yet, request again + RequestCarInfo(carId); + } + + return driverReport; + } + + private void SetSessionInfo(MsgSessionInfo msg, bool isNewSession) { + _currentSession.ServerName = msg.ServerName; + _currentSession.TrackName = msg.Track; + _currentSession.TrackConfig = msg.TrackConfig; + _currentSession.SessionName = msg.Name; + _currentSession.SessionType = msg.SessionType; + _currentSession.SessionDuration = msg.SessionDuration; + _currentSession.LapCount = msg.Laps; + _currentSession.WaitTime = msg.WaitTime; + if (isNewSession) { + _currentSession.Timestamp = msg.CreationDate.ToUniversalTime().Ticks; + } + _currentSession.AmbientTemp = msg.AmbientTemp; + _currentSession.RoadTemp = msg.RoadTemp; + _currentSession.Weather = msg.Weather; + _currentSession.RealtimeUpdateInterval = (ushort)_settings.RealtimeUpdateInterval.TotalMilliseconds; + // TODO: Set MaxClients when added to msg + + // Here you might want to start a new session: + /*if (isNewSession && StartNewLogOnNewSession > 0 && this.Logger is IFileLog) { + ((IFileLog)this.Logger).StartLoggingToFile( + new DateTime(_currentSession.Timestamp, DateTimeKind.Utc).ToString("yyyyMMdd_HHmmss") + "_" + + _currentSession.TrackName + "_" + _currentSession.SessionName + ".log"); + }*/ + } + + private void FinalizeAndStartNewReport() { + try { + // Update PlayerConnections with results + foreach (var connection in _currentSession.Drivers) { + var laps = _currentSession.Laps.Where(l => l.ConnectionId == connection.ConnectionId).ToList(); + var validLaps = laps.Where(l => l.Cuts == 0).ToList(); + if (validLaps.Count > 0) { + connection.BestLap = validLaps.Min(l => l.LapTime); + } else if (_currentSession.SessionType != Game.SessionType.Race) { + // Temporarily set BestLap to MaxValue for easier sorting for qualifying/practice results + connection.BestLap = int.MaxValue; + } + + if (laps.Count > 0) { + connection.TotalTime = (uint)laps.Sum(l => l.LapTime); + connection.LapCount = laps.Max(l => l.LapNo); + connection.Incidents += laps.Sum(l => l.Cuts); + } + } + + if (_currentSession.SessionType == Game.SessionType.Race) { + ushort position = 1; + + // Compute start position + foreach (var connection in _currentSession.Drivers + .Where(d => d.ConnectedTimestamp >= 0 && d.ConnectedTimestamp <= _currentSession.Timestamp) + .OrderByDescending(d => d.StartSplinePosition)) { + connection.StartPosition = position++; + } + + foreach (var connection in _currentSession.Drivers + .Where(d => d.ConnectedTimestamp >= 0 && d.ConnectedTimestamp > _currentSession.Timestamp) + .OrderBy(d => d.ConnectedTimestamp)) { + connection.StartPosition = position++; + } + + foreach (var connection in _currentSession.Drivers.Where(d => d.ConnectedTimestamp < 0)) { + connection.StartPosition = position++; + } + + // Compute end position + position = 1; + var winnerLapCount = 0; + var winnerTime = 0U; + + var sortedDrivers = new List(_currentSession.Drivers.Count); + + sortedDrivers.AddRange(_currentSession.Drivers.Where(d => d.LapCount == _currentSession.LapCount).OrderBy(GetLastLapTimestamp)); + sortedDrivers.AddRange(_currentSession.Drivers.Where(d => d.LapCount != _currentSession.LapCount) + .OrderByDescending(d => d.LapCount).ThenByDescending(d => d.EndSplinePosition)); + + foreach (var connection in sortedDrivers) { + if (position == 1) { + winnerLapCount = connection.LapCount; + winnerTime = connection.TotalTime; + } + connection.Position = position++; + + if (connection.LapCount == winnerLapCount) { + // Is incorrect for players connected after race started + connection.Gap = FormatTimespan((int)connection.TotalTime - (int)winnerTime); + } else { + if (winnerLapCount - connection.LapCount == 1) { + connection.Gap = "1 lap"; + } else { + connection.Gap = (winnerLapCount - connection.LapCount) + " laps"; + } + } + } + } else { + ushort position = 1; + var winnerTime = 0U; + foreach (var connection in _currentSession.Drivers.OrderBy(d => d.BestLap)) { + if (position == 1) { + winnerTime = connection.BestLap; + } + + connection.Position = position++; + + if (connection.BestLap == int.MaxValue) { + connection.BestLap = 0; // reset best lap + } else { + connection.Gap = FormatTimespan((int)connection.BestLap - (int)winnerTime); + } + } + } + + if (_currentSession.Drivers.Count > 0) { + foreach (var handler in _sessionReportHandlers) { + try { + handler.HandleReport(_currentSession); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + } finally { + _previousSession = _currentSession; + _currentSession = new SessionInfo { + MaxClients = _previousSession.MaxClients // TODO: can be removed when MaxClients added to MsgSessionInfo + }; + _lastCarUpdateCarId = -1; + + foreach (var connection in _previousSession.Drivers) { + if (_carUsedByDictionary.TryGetValue(connection.CarId, out var found) && found == connection) { + var recreatedConnection = new DriverInfo { + ConnectionId = _currentSession.Drivers.Count, + ConnectedTimestamp = found.ConnectedTimestamp, + DisconnectedTimestamp = found.DisconnectedTimestamp, // should be not set yet + DriverGuid = found.DriverGuid, + DriverName = found.DriverName, + DriverTeam = found.DriverTeam, + CarId = found.CarId, + CarModel = found.CarModel, + CarSkin = found.CarSkin, + BallastKg = found.BallastKg, + IsAdmin = found.IsAdmin + }; + + _currentSession.Drivers.Add(recreatedConnection); + } + } + + // Clear the dictionary of cars currently used + _carUsedByDictionary.Clear(); + foreach (var recreatedConnection in _currentSession.Drivers) { + _carUsedByDictionary.Add(recreatedConnection.CarId, recreatedConnection); + } + } + } + + public bool IsConnected => _udp.Opened; + + public void Connect() { + lock (_lockObject) { + if (IsConnected) { + throw new Exception("PluginManager already connected"); + } + + ProtocolVersion = -1; + _udp.Open(_settings.ListeningPort, _settings.RemoteHostName, _settings.RemotePort, + MessageReceived, msg => Logging.Warning(msg)); + + try { + OnConnected(); + } catch (Exception ex) { + Logging.Warning(ex); + } + + foreach (var externalPlugin in _externalPlugins) { + try { + var externalPluginUdp = new DuplexUdpClient(); + externalPluginUdp.Open(externalPlugin.ListeningPort, externalPlugin.RemoteHostname, externalPlugin.RemotePort, + MessageReceivedFromExternalPlugin, msg => Logging.Warning(msg)); + _openExternalPlugins.Add(externalPlugin, externalPluginUdp); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + } + + private void MessageReceivedFromExternalPlugin(TimestampedBytes tsb) { + _udp.Send(tsb); + + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + AcMessageParser.Parse(tsb)); + } + } + + public void Disconnect() { + if (!IsConnected) { + throw new Exception("PluginManager is not connected"); + } + + _udp.Close(); + + lock (_lockObject) { + try { + foreach (var externalPluginUdp in _openExternalPlugins.Values) { + try { + externalPluginUdp.Close(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + _openExternalPlugins.Clear(); + } catch (Exception ex) { + Logging.Warning(ex); + } + + try { + OnDisconnected(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + + private void MessageReceived(TimestampedBytes data) { + LastServerActivity = DateTime.Now; + + lock (_lockObject) { + var msg = AcMessageParser.Parse(data); + + if (ProtocolVersion == -1 && (msg is MsgVersionInfo || msg is MsgSessionInfo)) { + if (msg is MsgVersionInfo info) { + ProtocolVersion = info.Version; + } else { + ProtocolVersion = ((MsgSessionInfo)msg).Version; + } + + if (ProtocolVersion != RequiredProtocolVersion) { + Disconnect(); + throw new Exception( + $"AcServer protocol version {ProtocolVersion} is different from the required protocol version {RequiredProtocolVersion}. Disconnecting…"); + } + } + + try { + switch (msg.Type) { + case ACSProtocol.MessageType.ACSP_SESSION_INFO: + OnSessionInfo((MsgSessionInfo)msg); + break; + case ACSProtocol.MessageType.ACSP_NEW_SESSION: + OnNewSession((MsgSessionInfo)msg); + break; + case ACSProtocol.MessageType.ACSP_NEW_CONNECTION: + OnNewConnection((MsgNewConnection)msg); + break; + case ACSProtocol.MessageType.ACSP_CONNECTION_CLOSED: + OnConnectionClosed((MsgConnectionClosed)msg); + break; + case ACSProtocol.MessageType.ACSP_CAR_UPDATE: + OnCarUpdate((MsgCarUpdate)msg); + break; + case ACSProtocol.MessageType.ACSP_CAR_INFO: + OnCarInfo((MsgCarInfo)msg); + break; + case ACSProtocol.MessageType.ACSP_LAP_COMPLETED: + OnLapCompleted((MsgLapCompleted)msg); + break; + case ACSProtocol.MessageType.ACSP_END_SESSION: + OnSessionEnded((MsgSessionEnded)msg); + break; + case ACSProtocol.MessageType.ACSP_CLIENT_EVENT: + OnCollision((MsgClientEvent)msg); + break; + case ACSProtocol.MessageType.ACSP_VERSION: + OnProtocolVersion((MsgVersionInfo)msg); + break; + case ACSProtocol.MessageType.ACSP_CLIENT_LOADED: + OnClientLoaded((MsgClientLoaded)msg); + break; + case ACSProtocol.MessageType.ACSP_CHAT: + OnChatMessage((MsgChat)msg); + break; + case ACSProtocol.MessageType.ACSP_ERROR: + OnServerError((MsgError)msg); + break; + case ACSProtocol.MessageType.ACSP_REALTIMEPOS_INTERVAL: + case ACSProtocol.MessageType.ACSP_GET_CAR_INFO: + case ACSProtocol.MessageType.ACSP_SEND_CHAT: + case ACSProtocol.MessageType.ACSP_BROADCAST_CHAT: + case ACSProtocol.MessageType.ACSP_GET_SESSION_INFO: + throw new Exception("Received unexpected MessageType (for a plugin): " + msg.Type); + case ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_CAR: + case ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_ENV: + case ACSProtocol.MessageType.ERROR_BYTE: + throw new Exception("Received wrong or unknown MessageType: " + msg.Type); + default: + throw new Exception("Received wrong or unknown MessageType: " + msg.Type); + } + } catch (Exception ex) { + Logging.Warning(ex); + } + + foreach (var externalPluginUdp in _openExternalPlugins.Values) { + externalPluginUdp.TrySend(data); + } + } + } + + private void OnConnected() { + try { + // If we do not receive the session Info in the next 3 seconds request info (async). + ThreadPool.QueueUserWorkItem(o => { + try { + Thread.Sleep(3000); + if (ProtocolVersion == -1) { + RequestSessionInfo(-1); + } + } catch (Exception e) { + Logging.Warning(e); + } + }); + + foreach (var plugin in _plugins) { + try { + plugin.OnConnected(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + // If the KeepAlive monitor is configured, we'll start a endless loop + // that will try to determine a killed acServer. + if (_settings.AcServerKeepAliveInterval > TimeSpan.Zero) { + ThreadPool.QueueUserWorkItem(o => { + while (true) { + // Sleeping for some seconds + Thread.Sleep(_settings.AcServerKeepAliveInterval); + + // Everything is ok if we had either didn’t see anything so far (server still off) + // or we had server activity in the past seconds. + if (LastServerActivity != DateTime.MinValue + && LastServerActivity + _settings.AcServerKeepAliveInterval < DateTime.Now) { + // Now if the last Server Activity is older than our KeepAlive interval + // we’ll initiate a version check — probably the cheapest way to detect + // if the server is alive. + // if there is a realtime interval set, this should only fire when the + // server is empty and idling around in a P/Q session. So some messages + // won’t hurt anybody. + + // Update: There is no Version Request? Damn. Then a session one? + RequestSessionInfo(-1); + + // Then we’ll give the server some time to answer this request, which + // should set the date. + Thread.Sleep(1500); + + // Still late? + if (LastServerActivity + _settings.AcServerKeepAliveInterval < DateTime.Now) { + // We’ll go to the “server is dead” state so we won’t repeat this until the next + // “real” timeout. + LastServerActivity = DateTime.MinValue; + + foreach (var plugin in _plugins) { + try { + plugin.OnAcServerTimeout(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } else { + // No? That means the server was silent for more than the KeepAlive range, but is still responsive. + // May be weird, but some plugins need to know this + foreach (var plugin in _plugins) { + try { + plugin.OnAcServerAlive(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + } + } + }); + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnDisconnected() { + try { + FinalizeAndStartNewReport(); + _currentSession.Drivers.Clear(); + _carUsedByDictionary.Clear(); + + foreach (var plugin in _plugins) { + try { + plugin.OnDisconnected(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnSessionInfo(MsgSessionInfo msg) { + try { + var firstSessionInfo = _currentSession.SessionType == 0; + SetSessionInfo(msg, firstSessionInfo); + if (firstSessionInfo) { + // First time we received session info, also enable real time update + if (_settings.RealtimeUpdateInterval > TimeSpan.Zero) { + EnableRealtimeReport(_settings.RealtimeUpdateInterval); + } + // request car info for all cars + for (var i = 0; i < _currentSession.MaxClients; i++) { + RequestCarInfo((byte)i); + } + } + + foreach (var plugin in _plugins) { + try { + plugin.OnSessionInfo(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnNewSession(MsgSessionInfo msg) { + try { + _nextSessionStarting = msg; + + var firstSessionInfo = _currentSession.SessionType == 0; + if (!firstSessionInfo && _settings.NewSessionStartDelay > TimeSpan.Zero) { + ThreadPool.QueueUserWorkItem(o => { + Thread.Sleep(_settings.NewSessionStartDelay); + StartNewSession(msg); + }); + } else { + StartNewSession(msg); + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void StartNewSession(MsgSessionInfo msg) { + lock (_lockObject) { + if (msg != _nextSessionStarting) { + return; + } + + _nextSessionStarting = null; + + try { + FinalizeAndStartNewReport(); + _currentSession.MissedSessionStart = false; + SetSessionInfo(msg, true); + + if (_settings.RealtimeUpdateInterval > TimeSpan.Zero) { + EnableRealtimeReport(_settings.RealtimeUpdateInterval); + } + + foreach (var plugin in _plugins) { + try { + plugin.OnNewSession(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + + private void OnSessionEnded(MsgSessionEnded msg) { + try { + foreach (var plugin in _plugins) { + try { + plugin.OnSessionEnded(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnNewConnection(MsgNewConnection msg) { + try { + var newConnection = new DriverInfo { + ConnectionId = _currentSession.Drivers.Count, + ConnectedTimestamp = DateTime.UtcNow.Ticks, + DriverGuid = msg.DriverGuid, + DriverName = msg.DriverName, + DriverTeam = string.Empty, // missing in msg + CarId = msg.CarId, + CarModel = msg.CarModel, + CarSkin = msg.CarSkin, + BallastKg = 0 // missing in msg + }; + + _currentSession.Drivers.Add(newConnection); + + if (_carUsedByDictionary.TryGetValue(newConnection.CarId, out var otherDriver)) { + // should not happen + Logging.Warning($"Car is already used by another driver (manager: {GetHashCode()}, cars dict.: {_carUsedByDictionary.GetHashCode()})"); + otherDriver.DisconnectedTimestamp = DateTime.UtcNow.Ticks; + _carUsedByDictionary[msg.CarId] = newConnection; + } else { + _carUsedByDictionary.Add(newConnection.CarId, newConnection); + } + + // request car info to get additional info and check when driver really is connected + RequestCarInfo(msg.CarId); + + foreach (var plugin in _plugins) { + try { + plugin.OnNewConnection(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnConnectionClosed(MsgConnectionClosed msg) { + try { + if (_carUsedByDictionary.TryGetValue(msg.CarId, out var driverReport)) { + if (msg.DriverGuid == driverReport.DriverGuid) { + driverReport.DisconnectedTimestamp = DateTime.UtcNow.Ticks; + _carUsedByDictionary.Remove(msg.CarId); + } else { + Logging.Warning("MsgOnConnectionClosed DriverGuid does not match Guid of connected driver"); + } + } else { + Logging.Warning("Car was not known to be in use"); + } + + foreach (var plugin in _plugins) { + try { + plugin.OnConnectionClosed(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnCarInfo(MsgCarInfo msg) { + try { + if (_carUsedByDictionary.TryGetValue(msg.CarId, out var driverReport)) { + driverReport.CarModel = msg.CarModel; + driverReport.CarSkin = msg.CarSkin; + driverReport.DriverName = msg.DriverName; + driverReport.DriverTeam = msg.DriverTeam; + driverReport.DriverGuid = msg.DriverGuid; + } + + foreach (var plugin in _plugins) { + try { + plugin.OnCarInfo(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnCarUpdate(MsgCarUpdate msg) { + try { + // We check if this is the first CarUpdate message for this round (they seem to be sent in a bulk and ordered by carId) + // If that's the case we trigger OnBulkCarUpdateFinished + // The trick with the connectedDriversCount is used as a fail-safe when single messages are received out of order + var connectedDriversCount = CurrentSession.Drivers.Count(d => d.IsConnected); + var isBulkUpdate = _lastCarUpdateCarId - msg.CarId >= connectedDriversCount / 2; + _lastCarUpdateCarId = msg.CarId; + + // Ignore updates in the first 10 seconds of the session + if (_nextSessionStarting == null && DateTime.UtcNow.Ticks - _currentSession.Timestamp > 10 * 10000000) { + if (isBulkUpdate) { + // Ok, this was the last one, so the last updates are like a snapshot within a millisecond or less. + // Great spot to examine positions, overtakes and stuff where multiple cars are compared to each other + + OnBulkCarUpdateFinished(); + + // In every case we let the plugins do their calculations - before even raising the OnCarUpdate(msg). This function could + // Take advantage of updated DriverInfos + foreach (var plugin in _plugins) { + try { + plugin.OnBulkCarUpdateFinished(); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + + var driver = GetDriverReportForCarId(msg.CarId); + driver.UpdatePosition(msg, _settings.RealtimeUpdateInterval); + + //if (sw == null) + //{ + // sw = new StreamWriter(@"c:\workspace\positions.csv"); + // sw.AutoFlush = true; + //} + //sw.WriteLine(ToSingle3(msg.WorldPosition).ToString() + ", " + ToSingle3(msg.Velocity).Length()); + + foreach (var plugin in _plugins) { + try { + plugin.OnCarUpdate(msg); + plugin.OnCarUpdate(driver); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnBulkCarUpdateFinished() { + // So we'll try to compare the cars towards each other, because currently all DriverInfos + // are up-to-date and comparable + + // First: CurrentDistanceToClosestCar + // We'll just do a simple list of the moving cars that is ordered by the SplinePos. This doesn't respect + // Finding the position across the finish line, but this is a minor thing for now + CurrentSession.Drivers.ForEach(x => x.CurrentDistanceToClosestCar = 0); + var sortedDrivers = CurrentSession.Drivers.Where(x => x.CurrentSpeed > 30).OrderBy(x => x.EndSplinePosition).ToArray(); + if (sortedDrivers.Length > 1) { + var prev = sortedDrivers[sortedDrivers.Length - 1]; + for (var i = 0; i < sortedDrivers.Length; i++) { + var next = sortedDrivers[i]; + var distance = (prev.LastPosition - next.LastPosition).Length(); + + if (prev.CurrentDistanceToClosestCar > distance || prev.CurrentDistanceToClosestCar == 0) { + prev.CurrentDistanceToClosestCar = distance; + } + + if (next.CurrentDistanceToClosestCar > distance || next.CurrentDistanceToClosestCar == 0) { + next.CurrentDistanceToClosestCar = distance; + } + + prev = next; + } + } + } + + private void OnCollision(MsgClientEvent msg) { + try { + // Ignore collisions in the first 10 seconds of the session + if (_nextSessionStarting == null && DateTime.UtcNow.Ticks - _currentSession.Timestamp > 10 * 10000000) { + var driver = GetDriverReportForCarId(msg.CarId); + var withOtherCar = msg.Subtype == (byte)ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_CAR; + + driver.Incidents += withOtherCar ? 2 : 1; // TODO: only if relVel > thresh + + DriverInfo driver2 = null; + if (withOtherCar) { + driver2 = GetDriverReportForCarId(msg.OtherCarId); + driver2.Incidents += 2; // TODO: only if relVel > thresh + } + + var incident = new IncidentInfo { + Type = msg.Subtype, + Timestamp = DateTime.UtcNow.Ticks, + ConnectionId1 = driver.ConnectionId, + ConnectionId2 = withOtherCar ? driver2.ConnectionId : -1, + ImpactSpeed = msg.RelativeVelocity, + WorldPosition = msg.WorldPosition, + RelPosition = msg.RelativePosition, + }; + + _currentSession.Incidents.Add(incident); + + foreach (var plugin in _plugins) { + try { + plugin.OnCollision(msg); + plugin.OnCollision(incident); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnLapCompleted(MsgLapCompleted msg) { + try { + var driver = GetDriverReportForCarId(msg.CarId); + + var lapLength = driver.OnLapCompleted(); + + ushort position = 0; + ushort lapNo = 0; + for (var i = 0; i < msg.LeaderboardSize; i++) { + if (msg.Leaderboard[i].CarId == msg.CarId) { + position = (byte)(i + 1); + lapNo = msg.Leaderboard[i].Laps; + break; + } + } + + if (!_currentSession.MissedSessionStart && _currentSession.SessionType == Game.SessionType.Race) { + // For race compute Position based on own info (better with disconnected drivers) + position = (ushort)(_currentSession.Laps.Count(l => l.LapNo == lapNo) + 1); + } + + var lap = new LapInfo { + ConnectionId = driver.ConnectionId, + Timestamp = DateTime.UtcNow.Ticks, + LapTime = msg.LapTime, + LapLength = lapLength, + LapNo = lapNo, + Position = position, + Cuts = msg.Cuts, + GripLevel = msg.GripLevel + }; + + _currentSession.Laps.Add(lap); + + foreach (var plugin in _plugins) { + try { + plugin.OnLapCompleted(msg); + plugin.OnLapCompleted(lap); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnClientLoaded(MsgClientLoaded msg) { + try { + foreach (var plugin in _plugins) { + try { + plugin.OnClientLoaded(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnChatMessage(MsgChat msg) { + try { + if (TryGetDriverInfo(msg.CarId, out var driver)) { + if (!driver.IsAdmin && !string.IsNullOrWhiteSpace(_settings.AdminPassword) + && msg.Message.StartsWith("/admin ", StringComparison.InvariantCultureIgnoreCase)) { + driver.IsAdmin = msg.Message.Substring("/admin ".Length).Equals(_settings.AdminPassword); + } + + if (driver.IsAdmin) { + if (msg.Message.StartsWith("/send_chat ", StringComparison.InvariantCultureIgnoreCase)) { + var carIdStartIdx = "/send_chat ".Length; + var carIdEndIdx = msg.Message.IndexOf(' ', carIdStartIdx); + if (carIdEndIdx > carIdStartIdx && byte.TryParse(msg.Message.Substring(carIdStartIdx, carIdEndIdx - carIdStartIdx), out var carId)) { + var chatMsg = msg.Message.Substring(carIdEndIdx); + SendChatMessage(carId, chatMsg); + } else { + SendChatMessage(msg.CarId, "Invalid car id provided"); + } + } else if (msg.Message.StartsWith("/broadcast ", StringComparison.InvariantCultureIgnoreCase)) { + var broadcastMsg = msg.Message.Substring("/broadcast ".Length); + BroadcastChatMessage(broadcastMsg); + } + } + } + + foreach (var plugin in _plugins) { + try { + plugin.OnChatMessage(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnProtocolVersion(MsgVersionInfo msg) { + try { + foreach (var plugin in _plugins) { + try { + plugin.OnProtocolVersion(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + private void OnServerError(MsgError msg) { + try { + if (_settings.LogServerErrors) { + Logging.Warning("Server error: " + msg.ErrorMessage); + } + + foreach (var plugin in _plugins) { + try { + plugin.OnServerError(msg); + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + + public void ProcessEnteredCommand(string cmd) { + lock (_lockObject) { + try { + foreach (var plugin in _plugins) { + try { + if (plugin.OnCommandEntered(cmd)) { + break; + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } catch (Exception ex) { + Logging.Warning(ex); + } + } + } + + #region Requests to the AcServer + public void RequestCarInfo(byte carId) { + var carInfoRequest = new RequestCarInfo { CarId = carId }; + _udp.Send(carInfoRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + carInfoRequest); + } + } + + public void BroadcastChatMessage(string msg) { + var chatRequest = new RequestBroadcastChat { ChatMessage = msg }; + _udp.Send(chatRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + chatRequest); + } + } + + public void SendChatMessage(byte carId, string msg) { + var chatRequest = new RequestSendChat { CarId = carId, ChatMessage = msg }; + _udp.Send(chatRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + chatRequest); + } + } + + [Localizable(false)] + public void AdminCommand(string command) { + var chatRequest = new RequestAdminCommand { Command = command }; + _udp.Send(chatRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + chatRequest); + } + } + + public void RestartSession() { + var chatRequest = new RequestRestartSession(); + _udp.Send(chatRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + chatRequest); + } + } + + public void NextSession() { + var chatRequest = new RequestNextSession(); + _udp.Send(chatRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + chatRequest); + } + } + + public void EnableRealtimeReport(TimeSpan interval) { + _settings.RealtimeUpdateInterval = interval; + _currentSession.RealtimeUpdateInterval = (ushort)interval.TotalMilliseconds; + + var enableRealtimeReportRequest = new RequestRealtimeInfo { Interval = (ushort)interval.TotalMilliseconds }; + _udp.Send(enableRealtimeReportRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + enableRealtimeReportRequest); + } + } + + /// + /// Request a SessionInfo object, use -1 for the current session + /// + /// + public void RequestSessionInfo(Int16 sessionIndex) { + var sessionRequest = new RequestSessionInfo { SessionIndex = sessionIndex }; + _udp.Send(sessionRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + sessionRequest); + } + } + + public void RequestKickDriverById(byte carId) { + var kickRequest = new RequestKickUser { CarId = carId }; + _udp.Send(kickRequest.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + kickRequest); + } + } + + public void RequestSetSession(RequestSetSession requestSetSession) { + _udp.Send(requestSetSession.ToBinary()); + if (_settings.LogServerRequests) { + Logging.Debug("Request: " + requestSetSession); + } + } + + public void BroadcastCspCommand(T command) where T : struct, ICspCommand { + BroadcastChatMessage(command.Serialize()); + } + + public void SendCspCommand(byte carId, T command) where T : struct, ICspCommand { + SendChatMessage(carId, command.Serialize()); + } + + public void BroadcastCspCommand(string commandSerialized) { + BroadcastChatMessage(commandSerialized); + } + + public void SendCspCommand(byte carId, string commandSerialized) { + SendChatMessage(carId, commandSerialized); + } + #endregion + + #region some helper methods + public static string FormatTimespan(int timespan) { + var minutes = timespan / 1000 / 60; + var seconds = (timespan - minutes * 1000 * 60) / 1000.0; + return $"{minutes:00}:{seconds:00.000}"; + } + + private long GetLastLapTimestamp(DriverInfo driver) { + var lapReport = _currentSession.Laps.FirstOrDefault(l => l.ConnectionId == driver.ConnectionId && l.LapNo == driver.LapCount); + if (lapReport != null) { + return lapReport.Timestamp; + } + return long.MaxValue; + } + #endregion + + public void Dispose() { + if (IsConnected) { + Disconnect(); + } + foreach (var plugin in _plugins) { + plugin.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/AcPlugins/AcServerPluginManagerSettings.cs b/AcPlugins/AcServerPluginManagerSettings.cs new file mode 100644 index 0000000..b42466a --- /dev/null +++ b/AcPlugins/AcServerPluginManagerSettings.cs @@ -0,0 +1,43 @@ +using System; +using JetBrains.Annotations; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins { + public class AcServerPluginManagerSettings { + /// + /// Gets or sets the port on which the plugin manager receives messages from the AC server. + /// + public int ListeningPort { get; set; } + + /// + /// Gets or sets the port of the AC server where requests should be send to. + /// + public int RemotePort { get; set; } + + [CanBeNull] + public string AdminPassword { get; set; } + + public int Capacity { get; set; } + + /// + /// Gets or sets the hostname of the AC server. + /// + public string RemoteHostName { get; set; } = "127.0.0.1"; + + public TimeSpan RealtimeUpdateInterval { get; set; } = TimeSpan.FromSeconds(0.1); + + public TimeSpan NewSessionStartDelay { get; set; } = TimeSpan.FromSeconds(3d); + + /// + /// Gets or sets whether requests to the AC server should be logged. + /// + public bool LogServerRequests { get; set; } = true; + + public bool LogServerErrors { get; set; } = true; + + /// + /// Keep alive interval; if this is set to something > 0 the Plugin will + /// monitor the server and send a ServerTimeout if it's missing + /// + public TimeSpan AcServerKeepAliveInterval { get; set; } + } +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/CommandHandshakeIn.cs b/AcPlugins/CspCommands/CommandHandshakeIn.cs new file mode 100644 index 0000000..3bba5ee --- /dev/null +++ b/AcPlugins/CspCommands/CommandHandshakeIn.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + // Command sent from server to client, if values are not fitting, AC will close with a message. + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CommandHandshakeIn : ICspCommand { + public uint MinVersion; // build code + public bool RequiresWeatherFX; + + ushort ICspCommand.GetMessageType() { + return 0; + } + }; +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/CommandHandshakeOut.cs b/AcPlugins/CspCommands/CommandHandshakeOut.cs new file mode 100644 index 0000000..01f2ae8 --- /dev/null +++ b/AcPlugins/CspCommands/CommandHandshakeOut.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + // This command will be sent back as a response to first command. Format is the same. + // If you want to kick people with older version or without WeatherFX, add that version and + // WeatherFX flag to CommandHandshakeIn, but also remember to check CommandHandshakeOut, + // people might be using older version of CSP or not using it at all. + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CommandHandshakeOut : ICspCommand { + public uint Version; + public bool IsWeatherFXActive; + + ushort ICspCommand.GetMessageType() { + return 1; + } + }; +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/CommandWeatherSetV1.cs b/AcPlugins/CspCommands/CommandWeatherSetV1.cs new file mode 100644 index 0000000..374b3d2 --- /dev/null +++ b/AcPlugins/CspCommands/CommandWeatherSetV1.cs @@ -0,0 +1,39 @@ +using System.Runtime.InteropServices; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CommandWeatherSetV1 : ICspCommand { + /** + * Timestamp in unix format (number of seconds since 1/1/1970). + */ + public ulong Timestamp; + + /** + * Current weather type. + */ + public CommandWeatherType WeatherCurrent; + + /** + * Upcoming weather type. Please note: after finishing transition from A to B, next pair should be Current=B and Next=C. If + * both current and next weather would change to something new, transition will not be smooth. + */ + public CommandWeatherType WeatherNext; + + /** + * Transition between current and next weather, from 0 to 1. For smoother transition might make sense to apply some sort of + * smoothstep() function instead of sending linear value. + */ + public float Transition; + + /** + * If non-zero, upon receiving package CSP will apply conditions smoothly, using this time (in seconds) for transition. Best to + * use the same value as your update period. + * For sharp change (let’s say, upon session reset or switch) set to 0. + */ + public float TimeToApply; + + ushort ICspCommand.GetMessageType() { + return 1000; + } + } +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/CommandWeatherSetV2.cs b/AcPlugins/CspCommands/CommandWeatherSetV2.cs new file mode 100644 index 0000000..029192c --- /dev/null +++ b/AcPlugins/CspCommands/CommandWeatherSetV2.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.InteropServices; +using AcTools.ServerPlugin.DynamicConditions.Utils; +using SystemHalf; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct CommandWeatherSetV2 : ICspCommand { + public ulong Timestamp; + public CommandWeatherType WeatherCurrent; + public CommandWeatherType WeatherNext; + public ushort Transition; + public Half TimeToApply; + public Half TemperatureAmbient, TemperatureRoad; + public byte TrackGripEnc; + public byte Humidity; + public Half WindDirectionDeg, WindSpeedKmh; + public Half Pressure; + public Half RainIntensity, RainWetness, RainWater; + + public double TrackGrip { + set => TrackGripEnc = (byte)Math.Round(value.LerpInvSat(0.6, 1) * 255d); + } + + ushort ICspCommand.GetMessageType() { + return 1001; + } + } +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/CommandWeatherType.cs b/AcPlugins/CspCommands/CommandWeatherType.cs new file mode 100644 index 0000000..982eab4 --- /dev/null +++ b/AcPlugins/CspCommands/CommandWeatherType.cs @@ -0,0 +1,37 @@ +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + public enum CommandWeatherType : byte { + LightThunderstorm = 0, + Thunderstorm = 1, + HeavyThunderstorm = 2, + LightDrizzle = 3, + Drizzle = 4, + HeavyDrizzle = 5, + LightRain = 6, + Rain = 7, + HeavyRain = 8, + LightSnow = 9, + Snow = 10, + HeavySnow = 11, + LightSleet = 12, + Sleet = 13, + HeavySleet = 14, + Clear = 15, + FewClouds = 16, + ScatteredClouds = 17, + BrokenClouds = 18, + OvercastClouds = 19, + Fog = 20, + Mist = 21, + Smoke = 22, + Haze = 23, + Sand = 24, + Dust = 25, + Squalls = 26, + Tornado = 27, + Hurricane = 28, + Cold = 29, + Hot = 30, + Windy = 31, + Hail = 32 + } +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/CspCommandsUtils.cs b/AcPlugins/CspCommands/CspCommandsUtils.cs new file mode 100644 index 0000000..02b4268 --- /dev/null +++ b/AcPlugins/CspCommands/CspCommandsUtils.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.InteropServices; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + public static class CspCommandsUtils { + private static byte[] GetBytes(T str) where T : struct, ICspCommand { + var size = Marshal.SizeOf(str); + var arr = new byte[size + 2]; // reserving two extra bytes for type + IntPtr ptr = Marshal.AllocHGlobal(size); + Marshal.StructureToPtr(str, ptr, true); + Marshal.Copy(ptr, arr, 2, size); // writing structure with two bytes offset + Marshal.FreeHGlobal(ptr); + var typeBytes = BitConverter.GetBytes(str.GetMessageType()); + arr[0] = typeBytes[0]; // copying two bytes + arr[1] = typeBytes[1]; // there should definitely be a better way, but I can’t remember it at the moment + return arr; + } + + public static string Serialize(this T cmd) where T : struct, ICspCommand { + var bytes = GetBytes(cmd); + return "\t\t\t\t$CSP0:" + Convert.ToBase64String(bytes).TrimEnd('='); + } + } +} \ No newline at end of file diff --git a/AcPlugins/CspCommands/ICspCommand.cs b/AcPlugins/CspCommands/ICspCommand.cs new file mode 100644 index 0000000..d3ae738 --- /dev/null +++ b/AcPlugins/CspCommands/ICspCommand.cs @@ -0,0 +1,5 @@ +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands { + public interface ICspCommand { + ushort GetMessageType(); + } +} \ No newline at end of file diff --git a/AcPlugins/ExternalPluginInfo.cs b/AcPlugins/ExternalPluginInfo.cs new file mode 100644 index 0000000..d6a357c --- /dev/null +++ b/AcPlugins/ExternalPluginInfo.cs @@ -0,0 +1,13 @@ +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins { + public class ExternalPluginInfo { + public readonly int ListeningPort; + public readonly string RemoteHostname; + public readonly int RemotePort; + + public ExternalPluginInfo(int listeningPort, string remoteHostname, int remotePort) { + ListeningPort = listeningPort; + RemoteHostname = remoteHostname; + RemotePort = remotePort; + } + } +} \ No newline at end of file diff --git a/AcPlugins/Helpers/AcMessageParser.cs b/AcPlugins/Helpers/AcMessageParser.cs new file mode 100644 index 0000000..dda2e88 --- /dev/null +++ b/AcPlugins/Helpers/AcMessageParser.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers { + public class AcMessageParser { + public static PluginMessage Parse(TimestampedBytes rawMessage) { + var rawData = rawMessage.RawData; + + if (rawData == null) { + throw new ArgumentNullException(); + } + + if (rawData.Length == 0) { + throw new ArgumentException("rawData is empty"); + } + + ACSProtocol.MessageType msgType; + try { + msgType = (ACSProtocol.MessageType)rawData[0]; + } catch (Exception) { + throw new Exception("Message of not implemented type: " + rawData[0]); + } + + var newMsg = CreateInstance(msgType); + newMsg.CreationDate = rawMessage.IncomingDate; + using (var m = new MemoryStream(rawData)) + using (var br = new BinaryReader(m)) { + if (br.ReadByte() != (byte)newMsg.Type) { + throw new Exception("Can’t parse the message properly: " + newMsg.GetType().Name); + } + newMsg.Deserialize(br); + } + + return newMsg; + } + + private static PluginMessage CreateInstance(ACSProtocol.MessageType msgType) { + switch (msgType) { + case ACSProtocol.MessageType.ACSP_VERSION: + return new MsgVersionInfo(); + case ACSProtocol.MessageType.ACSP_SESSION_INFO: + return new MsgSessionInfo(); + case ACSProtocol.MessageType.ACSP_NEW_SESSION: + return new MsgSessionInfo(ACSProtocol.MessageType.ACSP_NEW_SESSION); + case ACSProtocol.MessageType.ACSP_NEW_CONNECTION: + return new MsgNewConnection(); + case ACSProtocol.MessageType.ACSP_CONNECTION_CLOSED: + return new MsgConnectionClosed(); + case ACSProtocol.MessageType.ACSP_CAR_UPDATE: + return new MsgCarUpdate(); + case ACSProtocol.MessageType.ACSP_CAR_INFO: + return new MsgCarInfo(); + case ACSProtocol.MessageType.ACSP_LAP_COMPLETED: + return new MsgLapCompleted(); + case ACSProtocol.MessageType.ACSP_END_SESSION: + return new MsgSessionEnded(); + case ACSProtocol.MessageType.ACSP_CLIENT_EVENT: + return new MsgClientEvent(); + case ACSProtocol.MessageType.ACSP_REALTIMEPOS_INTERVAL: + return new RequestRealtimeInfo(); + case ACSProtocol.MessageType.ACSP_GET_CAR_INFO: + return new RequestCarInfo(); + case ACSProtocol.MessageType.ACSP_SEND_CHAT: + return new RequestSendChat(); + case ACSProtocol.MessageType.ACSP_BROADCAST_CHAT: + return new RequestBroadcastChat(); + case ACSProtocol.MessageType.ACSP_ADMIN_COMMAND: + return new RequestAdminCommand(); + case ACSProtocol.MessageType.ACSP_NEXT_SESSION: + return new RequestNextSession(); + case ACSProtocol.MessageType.ACSP_RESTART_SESSION: + return new RequestRestartSession(); + case ACSProtocol.MessageType.ACSP_CHAT: + return new MsgChat(); + case ACSProtocol.MessageType.ACSP_GET_SESSION_INFO: + return new RequestSessionInfo(); + case ACSProtocol.MessageType.ACSP_CLIENT_LOADED: + return new MsgClientLoaded(); + case ACSProtocol.MessageType.ACSP_SET_SESSION_INFO: + return new RequestSetSession(); + case ACSProtocol.MessageType.ACSP_ERROR: + return new MsgError(); + case ACSProtocol.MessageType.ACSP_KICK_USER: + return new RequestKickUser(); + case ACSProtocol.MessageType.ERROR_BYTE: + throw new Exception("CreateInstance: MessageType is not set or wrong (ERROR_BYTE=0)"); + case ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_CAR: + case ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_ENV: + throw new Exception("CreateInstance: MessageType " + msgType + + " is not meant to be used as MessageType, but as Subtype to ACSP_CLIENT_EVENT"); + default: + throw new Exception("MessageType " + msgType + " is not known or implemented"); + } + } + } +} \ No newline at end of file diff --git a/AcPlugins/Helpers/DuplexUdpClient.cs b/AcPlugins/Helpers/DuplexUdpClient.cs new file mode 100644 index 0000000..5838630 --- /dev/null +++ b/AcPlugins/Helpers/DuplexUdpClient.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers { + public class DuplexUdpClient { + private UdpClient _plugin; + + private readonly Queue _messageQueue = new Queue(); + private readonly Queue _sendMessageQueue = new Queue(); + + public delegate void MessageReceivedDelegate(TimestampedBytes data); + + public delegate void ErrorHandlerDelegate(Exception ex); + + private MessageReceivedDelegate _messageReceived; + private ErrorHandlerDelegate _errorHandler; + private IPEndPoint _remoteIpEndPoint = null; + private Thread _processMessagesThread; + private Thread _receiveMessagesThread; + private Thread _sendMessagesThread; + + public int MinWaitMsBetweenSend { get; set; } + + public bool Opened { get; private set; } + + public void Open(int listeningPort, string remoteHostname, int remotePort, MessageReceivedDelegate callback, ErrorHandlerDelegate errorHandler, + int minWaitMsBetweenSend = 20) { + if (_plugin != null) { + throw new Exception("UdpServer was already started."); + } + + MinWaitMsBetweenSend = minWaitMsBetweenSend; + _messageReceived = callback; + _errorHandler = errorHandler; + + _plugin = new UdpClient(listeningPort); + _plugin.Connect(remoteHostname, remotePort); + _remoteIpEndPoint = new IPEndPoint(IPAddress.Any, remotePort); + Opened = true; + + _processMessagesThread = new Thread(ProcessMessages) { Name = "ProcessMessages", IsBackground = true }; + _processMessagesThread.Start(); + + _receiveMessagesThread = new Thread(ReceiveMessages) { Name = "ReceiveMessages", IsBackground = true }; + _receiveMessagesThread.Start(); + + _sendMessagesThread = new Thread(SendMessages) { Name = "SendMessages", IsBackground = true }; + _sendMessagesThread.Start(); + } + + public void Close() { + if (_plugin != null) { + lock (_sendMessageQueue) { + lock (_messageQueue) { + Opened = false; + Monitor.Pulse(_messageQueue); // if the ProcessMessages thread is waiting, wake it up + Monitor.Pulse(_sendMessageQueue); // if the SendMessages thread is waiting, wake it up + } + } + + _plugin.Close(); // _plugin.Receive in ReceiveMessages thread should return at this point + + if (Thread.CurrentThread != _processMessagesThread) { + if (!_processMessagesThread.Join(1000)) { + _processMessagesThread.Abort(); // make sure thread has terminated + } + } + + if (!_receiveMessagesThread.Join(1000)) { + _receiveMessagesThread.Abort(); // make sure thread has terminated + } + + if (!_sendMessagesThread.Join(1000)) { + _sendMessagesThread.Abort(); // make sure thread has terminated + } + + _plugin = null; + _messageQueue.Clear(); + _sendMessageQueue.Clear(); + } + } + + private void ProcessMessages() { + while (Opened) { + TimestampedBytes msgData; + lock (_messageQueue) { + if (_messageQueue.Count == 0) { + if (!Opened) break; // don't start waiting and exit loop if closed + Monitor.Wait(_messageQueue); + if (!Opened) break; // exit loop if closed + } + + msgData = _messageQueue.Dequeue(); + } + + try { + _messageReceived(msgData); + } catch (Exception ex) { + _errorHandler(ex); + } + } + } + + private void ReceiveMessages() { + while (Opened) { + try { + var bytesReceived = _plugin.Receive(ref _remoteIpEndPoint); + var tsb = new TimestampedBytes(bytesReceived); + lock (_messageQueue) { + _messageQueue.Enqueue(tsb); + Monitor.Pulse(_messageQueue); + } + } catch (Exception ex) { + if (Opened) { + // it seems the acServer is not running/ready yet + _errorHandler(ex); + } + } + } + } + + private void SendMessages() { + while (Opened) { + byte[] msgData; + lock (_sendMessageQueue) { + if (_sendMessageQueue.Count == 0) { + if (!Opened) break; // don't start waiting and exit loop if closed + Monitor.Wait(_sendMessageQueue); + if (!Opened) break; // exit loop if closed + } + + msgData = _sendMessageQueue.Dequeue().RawData; + } + + try { + _plugin.Send(msgData, msgData.Length); + if (MinWaitMsBetweenSend > 0) { + Thread.Sleep(MinWaitMsBetweenSend); + } + } catch (Exception ex) { + _errorHandler(ex); + } + } + } + + public void Send(TimestampedBytes dgram) { + if (_plugin == null) + throw new Exception("TrySend: UdpClient missing, please open first"); + + lock (_sendMessageQueue) { + _sendMessageQueue.Enqueue(dgram); + Monitor.Pulse(_sendMessageQueue); + } + } + + public bool TrySend(TimestampedBytes dgram) { + try { + Send(dgram); + return true; // we don't really know if it worked + } catch (Exception ex) { + _errorHandler(ex); + return false; + } + } + } +} \ No newline at end of file diff --git a/AcPlugins/Helpers/ISessionReportHandler.cs b/AcPlugins/Helpers/ISessionReportHandler.cs new file mode 100644 index 0000000..e58f76e --- /dev/null +++ b/AcPlugins/Helpers/ISessionReportHandler.cs @@ -0,0 +1,7 @@ +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers { + public interface ISessionReportHandler { + void HandleReport(SessionInfo report); + } +} \ No newline at end of file diff --git a/AcPlugins/Helpers/TimestampedBytes.cs b/AcPlugins/Helpers/TimestampedBytes.cs new file mode 100644 index 0000000..be35a95 --- /dev/null +++ b/AcPlugins/Helpers/TimestampedBytes.cs @@ -0,0 +1,18 @@ +using System; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers { + public class TimestampedBytes { + public byte[] RawData; + public DateTime IncomingDate; + + public TimestampedBytes(byte[] rawData) { + RawData = rawData; + IncomingDate = DateTime.Now; + } + + public TimestampedBytes(byte[] rawData, DateTime incomingDate) { + RawData = rawData; + IncomingDate = incomingDate; + } + } +} \ No newline at end of file diff --git a/AcPlugins/Helpers/Vector3F.cs b/AcPlugins/Helpers/Vector3F.cs new file mode 100644 index 0000000..2caca6a --- /dev/null +++ b/AcPlugins/Helpers/Vector3F.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.Serialization; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers { + [DataContract] + public struct Vector3F { + [DataMember] + public float X { get; set; } + + [DataMember] + public float Y { get; set; } + + [DataMember] + public float Z { get; set; } + + public Vector3F(float x, float y, float z) { + X = x; + Y = y; + Z = z; + } + + public float Length() { + return (float)Math.Sqrt(X * X + Y * Y + Z * Z); + } + + public static Vector3F operator +(Vector3F a, Vector3F b) { + return new Vector3F(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + } + + public static Vector3F operator -(Vector3F a, Vector3F b) { + return new Vector3F(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + } + + public override string ToString() { + return $"[{X} , {Y} , {Z}]"; + } + + private static Random R = new Random(); + + public static Vector3F RandomSmall() { + return new Vector3F() { + X = (float)(R.NextDouble() - 0.5) * 10, + Y = (float)(R.NextDouble() - 0.5), + Z = (float)(R.NextDouble() - 0.5) * 10, + }; + } + + public static Vector3F RandomBig() { + return new Vector3F() { + X = (float)(R.NextDouble() - 0.5) * 1000, + Y = (float)(R.NextDouble() - 0.5) * 20, + Z = (float)(R.NextDouble() - 0.5) * 1000, + }; + } + } +} \ No newline at end of file diff --git a/AcPlugins/Info/DriverInfo.cs b/AcPlugins/Info/DriverInfo.cs new file mode 100644 index 0000000..fabf420 --- /dev/null +++ b/AcPlugins/Info/DriverInfo.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info { + [DataContract] + public class DriverInfo { + #region MsgCarUpdate cache - No idea how we use this, and if it's cool at all + /// + /// Defines how many MsgCarUpdates are cached (for a look in the past) + /// + [IgnoreDataMember] + public static int MsgCarUpdateCacheSize { get; set; } = 0; + + [IgnoreDataMember] + private LinkedList _carUpdateCache = new LinkedList(); + + public LinkedListNode LastCarUpdate => _carUpdateCache.Last; + #endregion + + private const double MaxSpeed = 1000; // km/h + private const double MinSpeed = 5; // km/h + + [DataMember] + public int ConnectionId { get; set; } + + [DataMember] + public long ConnectedTimestamp { get; set; } = -1; + + [DataMember] + public long DisconnectedTimestamp { get; set; } = -1; + + [DataMember] + public string DriverGuid { get; set; } + + [DataMember] + public string DriverName { get; set; } + + [DataMember] + public string DriverTeam { get; set; } // currently not set + + [DataMember] + public byte CarId { get; set; } + + [DataMember] + public string CarModel { get; set; } + + [DataMember] + public string CarSkin { get; set; } + + [DataMember] + public ushort BallastKg { get; set; } // currently not set + + [DataMember] + public uint BestLap { get; set; } + + [DataMember] + public uint TotalTime { get; set; } + + [DataMember] + public ushort LapCount { get; set; } + + [DataMember] + public ushort StartPosition { get; set; } // only set for race session + + [DataMember] + public ushort Position { get; set; } // rename to e.g. Grid- or RacePosition? Easily mixed up with the Vector3 Positions + + [DataMember] + public string Gap { get; set; } + + [DataMember] + public int Incidents { get; set; } + + [DataMember] + public float Distance { get; set; } + + [IgnoreDataMember] + public float CurrentSpeed { get; set; } // km/h + + [IgnoreDataMember] + public float CurrentAcceleration { get; set; } // km/h + + [IgnoreDataMember] + public DateTime CurrentLapStart { get; set; } = DateTime.Now; + + [DataMember] + public float TopSpeed { get; set; } // km/h + + [DataMember] + public float StartSplinePosition { get; set; } = -1f; + + [DataMember] + public float EndSplinePosition { get; set; } = -1f; + + [DataMember] + public bool IsAdmin { get; set; } + + /// + /// IMPORTANT: Is not automatically set! The plugin is responsible to determine this as we have no official + /// way to do so by now. Just a field your plugin CAN use if you gather the information. + /// + [DataMember] + public bool IsOnOutlap { get; set; } + + public bool IsConnected => ConnectedTimestamp != -1 && DisconnectedTimestamp == -1; + + public string BestLapText => AcServerPluginManager.FormatTimespan((int)BestLap); + + private int _lastTime = -1; + private Vector3F _lastPosition; + private Vector3F _lastVelocity; + private float _lastSplinePos; + + private float _lapDistance; + private float _lastDistanceTraveled; + private float _lapStartSplinePos = -1f; + + #region getter for some 'realtime' positional info + public float LapDistance => _lapDistance; + + [IgnoreDataMember] + public float LastDistanceTraveled => _lastDistanceTraveled; + + public float LapStartSplinePos => _lapStartSplinePos; + + /// + /// of the last position update. + /// + public int LastPositionUpdate => _lastTime; + + public Vector3F LastPosition => _lastPosition; + + public Vector3F LastVelocity => _lastVelocity; + + public float LastSplinePosition => _lastSplinePos; + + /// + /// Expresses the distance in meters to the nearest car, either in front or back, ignoring positions. + /// Zero if there is no other (moving) car + /// + [IgnoreDataMember] + public float CurrentDistanceToClosestCar { get; set; } + #endregion + + // That cache should be replaced by a cache that also stores + // the timestamp, otherwise calculations are always squishy (and e.g. dependent on the interval) + public void UpdatePosition(MsgCarUpdate msg, TimeSpan realtimeUpdateInterval) { + UpdatePosition(msg.WorldPosition, msg.Velocity, msg.NormalizedSplinePosition, realtimeUpdateInterval); + if (MsgCarUpdateCacheSize > 0) { + // We have to protect this cache from higher realtimeUpdateIntervals as requested + if (_carUpdateCache.Count == 0 + || (msg.CreationDate - _carUpdateCache.Last.Value.CreationDate).TotalMilliseconds >= realtimeUpdateInterval.TotalMilliseconds * 0.9991) { + var node = _carUpdateCache.AddLast(msg); + if (_carUpdateCache.Count > MsgCarUpdateCacheSize) { + _carUpdateCache.RemoveFirst(); + } + + if (_carUpdateCache.Count > 1) { + // We could easily do car-specifc stuff here, e.g. calculate the distance driven between the intervals, + // or a python-app like delta - maybe even a loss of control + } + } + } + } + + public void UpdatePosition(Vector3F pos, Vector3F vel, float s, TimeSpan realtimeUpdateInterval) { + if (StartSplinePosition == -1.0f) { + StartSplinePosition = s > 0.5f ? s - 1.0f : s; + } + + if (_lapStartSplinePos == -1.0f) { + _lapStartSplinePos = s > 0.5f ? s - 1.0f : s; + } + + // Determine the current speed in KpH (only valid if the update interval is 1s) + CurrentSpeed = vel.Length() * 3.6f; + if (CurrentSpeed < MaxSpeed && CurrentSpeed > TopSpeed) { + TopSpeed = CurrentSpeed; + } + + // Determine the current acceleration in Kph/s (only valid if the update interval is 1s) + var lastSpeed = _lastVelocity.Length() * 3.6f; + CurrentAcceleration = (CurrentSpeed - lastSpeed) / (float)realtimeUpdateInterval.TotalSeconds; + + // See https://msdn.microsoft.com/de-de/library/system.environment.tickcount%28v=vs.110%29.aspx + var currentTime = Environment.TickCount & Int32.MaxValue; + var elapsedSinceLastUpdate = currentTime - _lastTime; + if (_lastTime > 0 && elapsedSinceLastUpdate > 0 && elapsedSinceLastUpdate < 3 * realtimeUpdateInterval.TotalSeconds) { + var d = (pos - _lastPosition).Length(); + var speed = d / elapsedSinceLastUpdate / 1000 * 3.6f; + + // If the computed average speed since last update is not much bigger than the maximum of last vel and the current vel then no warp detected. + // in worst case warps that occur from near the pits (~50m) are not detected. + if (speed - Math.Max(CurrentSpeed, lastSpeed) < 180d * elapsedSinceLastUpdate / 1000) { + // no warp detected + _lapDistance += d; + Distance += d; + _lastDistanceTraveled = d; + + if (CurrentSpeed > MinSpeed) { + // don't update LastSplinePos if car is moving very slowly (was send to box?) + EndSplinePosition = s; + } + } else { + // Probably warped to box + _lapDistance = 0; + _lapStartSplinePos = s > 0.5f ? s - 1.0f : s; + CurrentLapStart = DateTime.Now; + } + } + _lastPosition = pos; + _lastVelocity = vel; + _lastSplinePos = s; + _lastTime = currentTime; + } + + public float OnLapCompleted() { + var lastSplinePos = EndSplinePosition; + if (lastSplinePos < 0.5) { + lastSplinePos += 1f; + } + + var splinePosDiff = lastSplinePos - _lapStartSplinePos; + var lapLength = _lapDistance / splinePosDiff; + + _lapStartSplinePos = lastSplinePos - 1f; + _lapDistance = 0f; + _lastSplinePos = 0.0f; + CurrentLapStart = DateTime.Now; + + return lapLength; + } + } +} \ No newline at end of file diff --git a/AcPlugins/Info/IncidentInfo.cs b/AcPlugins/Info/IncidentInfo.cs new file mode 100644 index 0000000..ca61f20 --- /dev/null +++ b/AcPlugins/Info/IncidentInfo.cs @@ -0,0 +1,28 @@ +using System.Runtime.Serialization; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info { + [DataContract] + public class IncidentInfo { + [DataMember] + public byte Type { get; set; } + + [DataMember] + public long Timestamp { get; set; } + + [DataMember] + public int ConnectionId1 { get; set; } + + [DataMember] + public int ConnectionId2 { get; set; } + + [DataMember] + public float ImpactSpeed { get; set; } + + [DataMember] + public Vector3F WorldPosition { get; set; } + + [DataMember] + public Vector3F RelPosition { get; set; } + } +} \ No newline at end of file diff --git a/AcPlugins/Info/LapInfo.cs b/AcPlugins/Info/LapInfo.cs new file mode 100644 index 0000000..1879962 --- /dev/null +++ b/AcPlugins/Info/LapInfo.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info { + [DataContract] + public class LapInfo { + [DataMember] + public int ConnectionId { get; set; } + + [DataMember] + public long Timestamp { get; set; } + + [DataMember] + public uint LapTime { get; set; } + + [DataMember] + public float LapLength { get; set; } + + [DataMember] + public ushort LapNo { get; set; } + + [DataMember] + public ushort Position { get; set; } + + [DataMember] + public byte Cuts { get; set; } + + [DataMember] + public float GripLevel { get; set; } + } +} \ No newline at end of file diff --git a/AcPlugins/Info/SessionInfo.cs b/AcPlugins/Info/SessionInfo.cs new file mode 100644 index 0000000..db7b97c --- /dev/null +++ b/AcPlugins/Info/SessionInfo.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using AcTools.ServerPlugin.DynamicConditions.Data; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Info { + [DataContract] + public class SessionInfo { + [DataMember] + public bool MissedSessionStart { get; set; } = true; + + [DataMember] + public string ServerName { get; set; } + + [DataMember] + public string TrackName { get; set; } + + [DataMember] + public string TrackConfig { get; set; } + + [DataMember] + public string SessionName { get; set; } + + [DataMember] + public Game.SessionType SessionType { get; set; } + + [DataMember] + public ushort SessionDuration { get; set; } + + [DataMember] + public ushort LapCount { get; set; } + + [DataMember] + public ushort WaitTime { get; set; } + + [DataMember] + public long Timestamp { get; set; } = DateTime.UtcNow.Ticks; + + [DataMember] + public byte AmbientTemp { get; set; } + + [DataMember] + public byte RoadTemp { get; set; } + + [DataMember] + public string Weather { get; set; } + + [DataMember] + public int MaxClients { get; set; } + + [DataMember] + public int RealtimeUpdateInterval { get; set; } + + [DataMember] + public List Drivers { get; set; } = new List(); + + [DataMember] + public List Laps { get; set; } = new List(); + + [DataMember] + public List Incidents { get; set; } = new List(); + + /// + /// Computes the distance to the closest opponent. + /// + /// The driver. + /// The closest opponent. + /// The distance in meters. + public float GetDistanceToClosestOpponent(DriverInfo driver, out DriverInfo opponent) { + opponent = this.Drivers.Where(d => d != driver + && Math.Abs(d.LastPositionUpdate - driver.LastPositionUpdate) < 2 * RealtimeUpdateInterval) + .OrderBy(d => (d.LastPosition - driver.LastPosition).Length()).FirstOrDefault(); + if (opponent != null) { + return (opponent.LastPosition - driver.LastPosition).Length(); + } else { + return float.MaxValue; + } + } + } +} \ No newline at end of file diff --git a/AcPlugins/Kunos/ACSProtocol.cs b/AcPlugins/Kunos/ACSProtocol.cs new file mode 100644 index 0000000..383adb1 --- /dev/null +++ b/AcPlugins/Kunos/ACSProtocol.cs @@ -0,0 +1,71 @@ +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos { + public class ACSProtocol { + public enum MessageType : byte { + // careful, we'll use byte as underlying datatype so we can just write the Type into the BinaryReader + ERROR_BYTE = 0, + ACSP_NEW_SESSION = 50, + ACSP_NEW_CONNECTION = 51, + ACSP_CONNECTION_CLOSED = 52, + ACSP_CAR_UPDATE = 53, + ACSP_CAR_INFO = 54, // Sent as response to ACSP_GET_CAR_INFO command + ACSP_END_SESSION = 55, + ACSP_LAP_COMPLETED = 73, + + // EVENTS + ACSP_CLIENT_EVENT = 130, + + // EVENT TYPES + ACSP_CE_COLLISION_WITH_CAR = 10, + ACSP_CE_COLLISION_WITH_ENV = 11, + + // COMMANDS + ACSP_REALTIMEPOS_INTERVAL = 200, + ACSP_GET_CAR_INFO = 201, + ACSP_SEND_CHAT = 202, // Sends chat to one car + ACSP_BROADCAST_CHAT = 203, // Sends chat to everybody + + // new in update 1.2.3: + ACSP_SESSION_INFO = 59, + + /// + /// The server fires one up at startup (assuming the plugin is already there), you can also later re request the protocol version with an ACS_VERSION command. However, I've also added the protocol version number to the ACSP_NEW_SESSION message because that used to be the entry point for the plugin. + /// + ACSP_VERSION = 56, + + /// + /// Called every time a chat message is received by the server. Useful to interact with the plugins. + /// + ACSP_CHAT = 57, + + /// + /// Fired by the server when the first position update arrives from a client, which means, it's done loading + /// + ACSP_CLIENT_LOADED = 58, + + /// + /// Use this to request a session info packet to be sent by the server + /// + ACSP_GET_SESSION_INFO = 204, + + /// + /// To change name, laps, time, wait time of a session. In theory this can change also the current session, but the client is not aware of that at the moment. This will open the door to have time limited races, although the support would be much more clearly implemented natively in AC and I have plans to do that. + /// I might decide to have the server refusing to change the current session info + /// + ACSP_SET_SESSION_INFO = 205, + + /// + /// The server will use this sending a stringW with the description of the problem. Used for stuff like out of bound indices and so on. + /// + ACSP_ERROR = 60, + + /// + /// Kicks this player + /// + ACSP_KICK_USER = 206, + + ACSP_NEXT_SESSION = 207, + ACSP_RESTART_SESSION = 208, + ACSP_ADMIN_COMMAND = 209, // Send message plus a stringW with the command + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgCarInfo.cs b/AcPlugins/Messages/MsgCarInfo.cs new file mode 100644 index 0000000..9e5fb04 --- /dev/null +++ b/AcPlugins/Messages/MsgCarInfo.cs @@ -0,0 +1,37 @@ +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgCarInfo : PluginMessage { + public byte CarId { get; set; } + public bool IsConnected { get; set; } + public string CarModel { get; set; } + public string CarSkin { get; set; } + public string DriverName { get; set; } + public string DriverTeam { get; set; } + public string DriverGuid { get; set; } + + public MsgCarInfo() + : base(ACSProtocol.MessageType.ACSP_CAR_INFO) { } + + protected internal override void Serialize(System.IO.BinaryWriter bw) { + bw.Write(CarId); + bw.Write(IsConnected); + WriteStringW(bw, CarModel); + WriteStringW(bw, CarSkin); + WriteStringW(bw, DriverName); + WriteStringW(bw, DriverTeam); + WriteStringW(bw, DriverGuid); + } + + protected internal override void Deserialize(System.IO.BinaryReader br) { + CarId = br.ReadByte(); + IsConnected = br.ReadBoolean(); + + CarModel = ReadStringW(br); + CarSkin = ReadStringW(br); + DriverName = ReadStringW(br); + DriverTeam = ReadStringW(br); + DriverGuid = ReadStringW(br); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgCarUpdate.cs b/AcPlugins/Messages/MsgCarUpdate.cs new file mode 100644 index 0000000..2ad5a85 --- /dev/null +++ b/AcPlugins/Messages/MsgCarUpdate.cs @@ -0,0 +1,37 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgCarUpdate : PluginMessage { + #region As-binary-members; we should reuse them exactly this way to stay efficient + public byte CarId { get; set; } + public Vector3F WorldPosition { get; set; } + public Vector3F Velocity { get; set; } + public byte Gear { get; set; } + public ushort EngineRpm { get; set; } + public float NormalizedSplinePosition { get; set; } + #endregion + + public MsgCarUpdate() + : base(ACSProtocol.MessageType.ACSP_CAR_UPDATE) { } + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + WorldPosition = ReadVector3F(br); + Velocity = ReadVector3F(br); + Gear = br.ReadByte(); + EngineRpm = br.ReadUInt16(); + NormalizedSplinePosition = br.ReadSingle(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + WriteVector3F(bw, WorldPosition); + WriteVector3F(bw, Velocity); + bw.Write(Gear); + bw.Write(EngineRpm); + bw.Write(NormalizedSplinePosition); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgChat.cs b/AcPlugins/Messages/MsgChat.cs new file mode 100644 index 0000000..2e73b45 --- /dev/null +++ b/AcPlugins/Messages/MsgChat.cs @@ -0,0 +1,33 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgChat : PluginMessage { + public MsgChat() + : base(ACSProtocol.MessageType.ACSP_CHAT) { } + + #region Members as binary + public byte CarId { get; private set; } + + public string Message { get; private set; } + #endregion + + public bool IsCommand { + get { + if (string.IsNullOrWhiteSpace(Message)) + return false; + return Message.StartsWith("/"); + } + } + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + Message = ReadStringW(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + bw.Write(Message); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgClientEvent.cs b/AcPlugins/Messages/MsgClientEvent.cs new file mode 100644 index 0000000..d82319a --- /dev/null +++ b/AcPlugins/Messages/MsgClientEvent.cs @@ -0,0 +1,49 @@ +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgClientEvent : PluginMessage { + public byte Subtype { get; set; } + public byte CarId { get; set; } + public byte OtherCarId { get; set; } + + public float RelativeVelocity { get; set; } + public Vector3F WorldPosition { get; set; } + public Vector3F RelativePosition { get; set; } + + public MsgClientEvent() + : base(ACSProtocol.MessageType.ACSP_CLIENT_EVENT) { } + + public MsgClientEvent(MsgClientEvent copy) + : base(ACSProtocol.MessageType.ACSP_CLIENT_EVENT) { + Subtype = copy.Subtype; + CarId = copy.CarId; + OtherCarId = copy.OtherCarId; + RelativeVelocity = copy.RelativeVelocity; + WorldPosition = copy.WorldPosition; // wrong, should really copy this + RelativePosition = copy.RelativePosition; // wrong, should really copy this + } + + protected internal override void Serialize(System.IO.BinaryWriter bw) { + bw.Write(Subtype); + bw.Write(CarId); + if (Subtype == (byte)ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_CAR) + bw.Write(OtherCarId); + + bw.Write(RelativeVelocity); + + WriteVector3F(bw, WorldPosition); + WriteVector3F(bw, RelativePosition); + } + + protected internal override void Deserialize(System.IO.BinaryReader br) { + Subtype = br.ReadByte(); + CarId = br.ReadByte(); + if (Subtype == (byte)ACSProtocol.MessageType.ACSP_CE_COLLISION_WITH_CAR) + OtherCarId = br.ReadByte(); + RelativeVelocity = br.ReadSingle(); + WorldPosition = ReadVector3F(br); + RelativePosition = ReadVector3F(br); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgClientLoaded.cs b/AcPlugins/Messages/MsgClientLoaded.cs new file mode 100644 index 0000000..9f13b8b --- /dev/null +++ b/AcPlugins/Messages/MsgClientLoaded.cs @@ -0,0 +1,21 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgClientLoaded : PluginMessage { + public MsgClientLoaded() + : base(ACSProtocol.MessageType.ACSP_CLIENT_LOADED) { } + + #region members as binary + public byte CarId { get; private set; } + #endregion + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgConnectionClosed.cs b/AcPlugins/Messages/MsgConnectionClosed.cs new file mode 100644 index 0000000..60ce356 --- /dev/null +++ b/AcPlugins/Messages/MsgConnectionClosed.cs @@ -0,0 +1,31 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgConnectionClosed : PluginMessage { + public string DriverName { get; set; } + public string DriverGuid { get; set; } + public byte CarId { get; set; } + public string CarModel { get; set; } + public string CarSkin { get; set; } + + public MsgConnectionClosed() + : base(ACSProtocol.MessageType.ACSP_CONNECTION_CLOSED) { } + + protected internal override void Deserialize(BinaryReader br) { + DriverName = ReadStringW(br); + DriverGuid = ReadStringW(br); + CarId = br.ReadByte(); + CarModel = ReadString(br); + CarSkin = ReadString(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + WriteStringW(bw, DriverName); + WriteStringW(bw, DriverGuid); + bw.Write(CarId); + WriteStringW(bw, CarModel); + WriteStringW(bw, CarSkin); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgError.cs b/AcPlugins/Messages/MsgError.cs new file mode 100644 index 0000000..98f5c9f --- /dev/null +++ b/AcPlugins/Messages/MsgError.cs @@ -0,0 +1,19 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgError : PluginMessage { + public string ErrorMessage { get; private set; } + + public MsgError() + : base(ACSProtocol.MessageType.ACSP_ERROR) { } + + protected internal override void Deserialize(BinaryReader br) { + ErrorMessage = ReadStringW(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(ErrorMessage); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgLapCompleted.cs b/AcPlugins/Messages/MsgLapCompleted.cs new file mode 100644 index 0000000..84407f0 --- /dev/null +++ b/AcPlugins/Messages/MsgLapCompleted.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgLapCompleted : PluginMessage { + public byte CarId { get; set; } + public uint LapTime { get; set; } + public byte Cuts { get; set; } + + public byte LeaderboardSize => (byte)Leaderboard.Count; + + public List Leaderboard { get; set; } + public float GripLevel { get; set; } + + public MsgLapCompleted() + : base(ACSProtocol.MessageType.ACSP_LAP_COMPLETED) { + Leaderboard = new List(); + } + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + LapTime = br.ReadUInt32(); + Cuts = br.ReadByte(); + + var leaderboardCount = br.ReadByte(); + Leaderboard.Clear(); + for (int i = 0; i < leaderboardCount; i++) { + Leaderboard.Add(MsgLapCompletedLeaderboardEnty.FromBinaryReader(br)); + } + + GripLevel = br.ReadSingle(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + bw.Write(LapTime); + bw.Write(Cuts); + bw.Write(LeaderboardSize); + + foreach (var entry in Leaderboard) { + entry.Serialize(bw); + } + + bw.Write(GripLevel); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgLapCompletedLeaderboardEnty.cs b/AcPlugins/Messages/MsgLapCompletedLeaderboardEnty.cs new file mode 100644 index 0000000..2b6452f --- /dev/null +++ b/AcPlugins/Messages/MsgLapCompletedLeaderboardEnty.cs @@ -0,0 +1,26 @@ +using System.IO; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgLapCompletedLeaderboardEnty { + public byte CarId { get; set; } + public uint Laptime { get; set; } + public ushort Laps { get; set; } + public bool HasFinished { get; set; } + + public static MsgLapCompletedLeaderboardEnty FromBinaryReader(BinaryReader br) { + return new MsgLapCompletedLeaderboardEnty() { + CarId = br.ReadByte(), + Laptime = br.ReadUInt32(), + Laps = br.ReadUInt16(), + HasFinished = br.ReadBoolean() + }; + } + + internal void Serialize(BinaryWriter bw) { + bw.Write(CarId); + bw.Write(Laptime); + bw.Write(Laps); + bw.Write(HasFinished); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgNewConnection.cs b/AcPlugins/Messages/MsgNewConnection.cs new file mode 100644 index 0000000..b0a301b --- /dev/null +++ b/AcPlugins/Messages/MsgNewConnection.cs @@ -0,0 +1,31 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgNewConnection : PluginMessage { + public string DriverName { get; set; } + public string DriverGuid { get; set; } + public byte CarId { get; set; } + public string CarModel { get; set; } + public string CarSkin { get; set; } + + public MsgNewConnection() + : base(ACSProtocol.MessageType.ACSP_NEW_CONNECTION) { } + + protected internal override void Deserialize(BinaryReader br) { + DriverName = ReadStringW(br); + DriverGuid = ReadStringW(br); + CarId = br.ReadByte(); + CarModel = ReadString(br); + CarSkin = ReadString(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + WriteStringW(bw, DriverName); + WriteStringW(bw, DriverGuid); + bw.Write(CarId); + WriteStringW(bw, CarModel); + WriteStringW(bw, CarSkin); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgNewSession.cs b/AcPlugins/Messages/MsgNewSession.cs new file mode 100644 index 0000000..481677c --- /dev/null +++ b/AcPlugins/Messages/MsgNewSession.cs @@ -0,0 +1,9 @@ +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgNewSession : MsgSessionInfo { + public MsgNewSession() { + Type = ACSProtocol.MessageType.ACSP_NEW_SESSION; + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgSessionEnded.cs b/AcPlugins/Messages/MsgSessionEnded.cs new file mode 100644 index 0000000..93a1884 --- /dev/null +++ b/AcPlugins/Messages/MsgSessionEnded.cs @@ -0,0 +1,20 @@ +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgSessionEnded : PluginMessage { + #region As-binary-members; we should reuse them exactly this way to stay efficient + public string ReportFileName { get; set; } + #endregion + + public MsgSessionEnded() + : base(ACSProtocol.MessageType.ACSP_END_SESSION) { } + + protected internal override void Serialize(System.IO.BinaryWriter bw) { + WriteStringW(bw, ReportFileName); + } + + protected internal override void Deserialize(System.IO.BinaryReader br) { + ReportFileName = ReadStringW(br); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgSessionInfo.cs b/AcPlugins/Messages/MsgSessionInfo.cs new file mode 100644 index 0000000..50f2ceb --- /dev/null +++ b/AcPlugins/Messages/MsgSessionInfo.cs @@ -0,0 +1,108 @@ +using System; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; +using AcTools.ServerPlugin.DynamicConditions.Data; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgSessionInfo : PluginMessage { + #region As-binary-members; we should reuse them exactly this way to stay efficient + public byte Version { get; set; } + public string ServerName { get; set; } + public string TrackConfig { get; set; } + public string Track { get; set; } + public string Name { get; set; } + public Game.SessionType SessionType { get; set; } + public ushort SessionDuration { get; set; } + public ushort Laps { get; set; } + public ushort WaitTime { get; set; } + public byte AmbientTemp { get; set; } + public byte RoadTemp { get; set; } + public string Weather { get; set; } + + /// + /// Milliseconds from the start (this might be negative for races with WaitTime) + /// + public int ElapsedMS { get; private set; } + #endregion + + #region wellformed stuff members - offer some more comfortable data conversion + public TimeSpan SessionDurationTimespan { + get { return TimeSpan.FromMinutes(SessionDuration); } + + set { SessionDuration = Convert.ToUInt16(Math.Round(value.TotalMinutes, 0)); } + } + + /// + /// The index of the session in the message + /// + public byte SessionIndex { get; private set; } + + /// + /// The index of the current session in the server + /// + public byte CurrentSessionIndex { get; private set; } + + /// + /// The number of sessions in the server + /// + public byte SessionCount { get; private set; } + #endregion + + public MsgSessionInfo() + : base(ACSProtocol.MessageType.ACSP_SESSION_INFO) { } + + public MsgSessionInfo(ACSProtocol.MessageType overridingNewSessionFlag) + : base(ACSProtocol.MessageType.ACSP_NEW_SESSION) { + if (overridingNewSessionFlag != ACSProtocol.MessageType.ACSP_NEW_SESSION) + throw new Exception("MsgSessionInfo's type may only be overriden by ACSP_NEW_SESSION"); + } + + protected internal override void Deserialize(System.IO.BinaryReader br) { + Version = br.ReadByte(); + SessionIndex = br.ReadByte(); + CurrentSessionIndex = br.ReadByte(); + SessionCount = br.ReadByte(); + ServerName = ReadStringW(br); + Track = ReadString(br); + TrackConfig = ReadString(br); + Name = ReadString(br); + SessionType = (Game.SessionType)br.ReadByte(); + SessionDuration = br.ReadUInt16(); + Laps = br.ReadUInt16(); + WaitTime = br.ReadUInt16(); + AmbientTemp = br.ReadByte(); + RoadTemp = br.ReadByte(); + Weather = ReadString(br); + ElapsedMS = br.ReadInt32(); + } + + protected internal override void Serialize(System.IO.BinaryWriter bw) { + bw.Write(Version); + bw.Write(SessionIndex); + bw.Write(CurrentSessionIndex); + bw.Write(SessionCount); + WriteStringW(bw, ServerName); + WriteString(bw, Track); + WriteString(bw, TrackConfig); + WriteString(bw, Name); + bw.Write((byte)SessionType); + bw.Write(SessionDuration); + bw.Write(Laps); + bw.Write(WaitTime); + bw.Write(AmbientTemp); + bw.Write(RoadTemp); + WriteString(bw, Weather); + bw.Write(ElapsedMS); + } + + public RequestSetSession CreateSetSessionRequest() { + return new RequestSetSession() { + Laps = Laps, + SessionName = Name, + SessionIndex = SessionIndex, + SessionType = SessionType, + Time = SessionDuration, + WaitTime = WaitTime, + }; + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/MsgVersionInfo.cs b/AcPlugins/Messages/MsgVersionInfo.cs new file mode 100644 index 0000000..ca6895c --- /dev/null +++ b/AcPlugins/Messages/MsgVersionInfo.cs @@ -0,0 +1,21 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class MsgVersionInfo : PluginMessage { + #region As-binary-members; we should reuse them exactly this way to stay efficient + public byte Version { get; set; } + #endregion + + public MsgVersionInfo() + : base(ACSProtocol.MessageType.ACSP_VERSION) { } + + protected internal override void Deserialize(BinaryReader br) { + Version = br.ReadByte(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(Version); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestAdminCommand.cs b/AcPlugins/Messages/RequestAdminCommand.cs new file mode 100644 index 0000000..631b1bc --- /dev/null +++ b/AcPlugins/Messages/RequestAdminCommand.cs @@ -0,0 +1,19 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestAdminCommand : PluginMessage { + public string Command { get; set; } + + public RequestAdminCommand() + : base(ACSProtocol.MessageType.ACSP_ADMIN_COMMAND) { } + + protected internal override void Deserialize(BinaryReader br) { + Command = ReadStringW(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + WriteStringW(bw, Command); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestBroadcastChat.cs b/AcPlugins/Messages/RequestBroadcastChat.cs new file mode 100644 index 0000000..63cd016 --- /dev/null +++ b/AcPlugins/Messages/RequestBroadcastChat.cs @@ -0,0 +1,19 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestBroadcastChat : PluginMessage { + public string ChatMessage { get; set; } + + public RequestBroadcastChat() + : base(ACSProtocol.MessageType.ACSP_BROADCAST_CHAT) { } + + protected internal override void Deserialize(BinaryReader br) { + ChatMessage = ReadStringW(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + WriteStringW(bw, ChatMessage); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestCarInfo.cs b/AcPlugins/Messages/RequestCarInfo.cs new file mode 100644 index 0000000..2b44c92 --- /dev/null +++ b/AcPlugins/Messages/RequestCarInfo.cs @@ -0,0 +1,19 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestCarInfo : PluginMessage { + public byte CarId { get; set; } + + public RequestCarInfo() + : base(ACSProtocol.MessageType.ACSP_GET_CAR_INFO) { } + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestKickUser.cs b/AcPlugins/Messages/RequestKickUser.cs new file mode 100644 index 0000000..201205f --- /dev/null +++ b/AcPlugins/Messages/RequestKickUser.cs @@ -0,0 +1,19 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestKickUser : PluginMessage { + public byte CarId { get; set; } + + public RequestKickUser() + : base(ACSProtocol.MessageType.ACSP_KICK_USER) { } + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestNextSession.cs b/AcPlugins/Messages/RequestNextSession.cs new file mode 100644 index 0000000..a957ec4 --- /dev/null +++ b/AcPlugins/Messages/RequestNextSession.cs @@ -0,0 +1,13 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestNextSession : PluginMessage { + public RequestNextSession() + : base(ACSProtocol.MessageType.ACSP_NEXT_SESSION) { } + + protected internal override void Deserialize(BinaryReader br) { } + + protected internal override void Serialize(BinaryWriter bw) { } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestRealtimeInfo.cs b/AcPlugins/Messages/RequestRealtimeInfo.cs new file mode 100644 index 0000000..7c09d20 --- /dev/null +++ b/AcPlugins/Messages/RequestRealtimeInfo.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestRealtimeInfo : PluginMessage { + public UInt16 Interval { get; set; } + + public RequestRealtimeInfo() + : base(ACSProtocol.MessageType.ACSP_REALTIMEPOS_INTERVAL) { } + + protected internal override void Deserialize(BinaryReader br) { + Interval = br.ReadUInt16(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(Interval); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestRestartSession.cs b/AcPlugins/Messages/RequestRestartSession.cs new file mode 100644 index 0000000..c4d3aeb --- /dev/null +++ b/AcPlugins/Messages/RequestRestartSession.cs @@ -0,0 +1,13 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestRestartSession : PluginMessage { + public RequestRestartSession() + : base(ACSProtocol.MessageType.ACSP_RESTART_SESSION) { } + + protected internal override void Deserialize(BinaryReader br) { } + + protected internal override void Serialize(BinaryWriter bw) { } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestSendChat.cs b/AcPlugins/Messages/RequestSendChat.cs new file mode 100644 index 0000000..b5346fd --- /dev/null +++ b/AcPlugins/Messages/RequestSendChat.cs @@ -0,0 +1,22 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestSendChat : PluginMessage { + public byte CarId { get; set; } + public string ChatMessage { get; set; } + + public RequestSendChat() + : base(ACSProtocol.MessageType.ACSP_SEND_CHAT) { } + + protected internal override void Deserialize(BinaryReader br) { + CarId = br.ReadByte(); + ChatMessage = ReadStringW(br); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(CarId); + WriteStringW(bw, ChatMessage); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestSessionInfo.cs b/AcPlugins/Messages/RequestSessionInfo.cs new file mode 100644 index 0000000..5fd146c --- /dev/null +++ b/AcPlugins/Messages/RequestSessionInfo.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestSessionInfo : PluginMessage { + /// + /// ACSP_GET_SESSION_INFO gets a session index you want to get or a -1 for the current session + /// + public Int16 SessionIndex { get; set; } + + public RequestSessionInfo() + : base(ACSProtocol.MessageType.ACSP_GET_SESSION_INFO) { } + + protected internal override void Deserialize(BinaryReader br) { + SessionIndex = br.ReadInt16(); + } + + protected internal override void Serialize(BinaryWriter bw) { + bw.Write(SessionIndex); + } + } +} \ No newline at end of file diff --git a/AcPlugins/Messages/RequestSetSession.cs b/AcPlugins/Messages/RequestSetSession.cs new file mode 100644 index 0000000..580c0b7 --- /dev/null +++ b/AcPlugins/Messages/RequestSetSession.cs @@ -0,0 +1,54 @@ +using System.IO; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; +using AcTools.ServerPlugin.DynamicConditions.Data; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages { + public class RequestSetSession : PluginMessage { + public RequestSetSession() + : base(ACSProtocol.MessageType.ACSP_SET_SESSION_INFO) { } + + public byte SessionIndex { get; set; } + public string SessionName { get; set; } + public Game.SessionType SessionType { get; set; } + public uint Laps { get; set; } + + /// + /// Time (of day?) in seconds + /// + public uint Time { get; set; } + + /// + /// Wait time (before race/lock pits in qualifying) in seconds + /// + public uint WaitTime { get; set; } + + protected internal override void Deserialize(BinaryReader br) { + SessionIndex = br.ReadByte(); + SessionName = ReadStringW(br); + SessionType = (Game.SessionType)br.ReadByte(); + Laps = br.ReadUInt32(); + Time = br.ReadUInt32(); + WaitTime = br.ReadUInt32(); + } + + protected internal override void Serialize(BinaryWriter bw) { + // Session Index we want to change, be very careful with changing the current session tho, some stuff might not work as expected + bw.Write(SessionIndex); + + // Session name + WriteStringW(bw, SessionName); // Careful here, the server is still broadcasting ASCII strings to the clients for this + + // Session type + bw.Write((byte)SessionType); + + // Laps + bw.Write(Laps); + + // Time (in seconds) + bw.Write(Time); + + // Wait time (in seconds) + bw.Write(WaitTime); + } + } +} \ No newline at end of file diff --git a/AcPlugins/PluginMessage.cs b/AcPlugins/PluginMessage.cs new file mode 100644 index 0000000..1a39ff3 --- /dev/null +++ b/AcPlugins/PluginMessage.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Text; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Helpers; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Kunos; + +namespace AcTools.ServerPlugin.DynamicConditions.AcPlugins { + public abstract class PluginMessage { + public ACSProtocol.MessageType Type { get; protected internal set; } + public DateTime CreationDate { get; protected internal set; } + + public PluginMessage(ACSProtocol.MessageType type) { + Type = type; + } + + public override string ToString() { + var s = ""; + foreach (var prop in GetType().GetProperties()) { + if (prop.Name != "StringRepresentation") + s += prop.Name + "=" + prop.GetValue(this, null) + Environment.NewLine; + } + return s; + } + + protected internal abstract void Serialize(BinaryWriter bw); + + protected internal abstract void Deserialize(BinaryReader br); + + public TimestampedBytes ToBinary() { + using (var m = new MemoryStream()) + using (var bw = new BinaryWriter(m)) { + bw.Write((byte)Type); + Serialize(bw); + return new TimestampedBytes(m.ToArray(), this.CreationDate); + } + } + + [Obsolete("Used nowhere?")] + public void FromBinary(TimestampedBytes data) { + using (var m = new MemoryStream(data.RawData)) + using (var br = new BinaryReader(m)) { + var type = br.Read(); + if ((byte)Type != type) + throw new Exception("FromBinary() Type != type"); + + Deserialize(br); + CreationDate = data.IncomingDate; + } + } + + #region Helpers: (write & read binary stuff) + protected static string ReadStringW(BinaryReader br) { + // Read the length, 1 byte + var length = br.ReadByte(); + + // Read the chars + return Encoding.UTF32.GetString(br.ReadBytes(length * 4)); + } + + protected static void WriteStringW(BinaryWriter bw, string message) { + bw.Write((byte)(message.Length)); + bw.Write(Encoding.UTF32.GetBytes(message)); + } + + protected static string ReadString(BinaryReader br) { + // Read the length, 1 byte + var length = br.ReadByte(); + + // Read the chars + return new string(br.ReadChars(length)); + } + + protected static void WriteString(BinaryWriter bw, string message) { + var array = message.ToCharArray(); + bw.Write((byte)array.Length); + bw.Write(array); + } + + protected static Vector3F ReadVector3F(BinaryReader br) { + return new Vector3F { + X = br.ReadSingle(), + Y = br.ReadSingle(), + Z = br.ReadSingle() + }; + } + + protected static void WriteVector3F(BinaryWriter bw, Vector3F vec) { + bw.Write(vec.X); + bw.Write(vec.Y); + bw.Write(vec.Z); + } + #endregion + } +} \ No newline at end of file diff --git a/AcTools.ServerPlugin.DynamicConditions.csproj b/AcTools.ServerPlugin.DynamicConditions.csproj new file mode 100644 index 0000000..3bd1818 --- /dev/null +++ b/AcTools.ServerPlugin.DynamicConditions.csproj @@ -0,0 +1,123 @@ + + + + + Debug + AnyCPU + {3478E974-CE23-4CC7-A41B-2E868F12378F} + Exe + Properties + AcTools.ServerPlugin.DynamicConditions + AcTools.ServerPlugin.DynamicConditions + v4.5.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + packages\JetBrains.Annotations.2022.1.0\lib\net20\JetBrains.Annotations.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AcTools.ServerPlugin.DynamicConditions.sln b/AcTools.ServerPlugin.DynamicConditions.sln new file mode 100644 index 0000000..4973af4 --- /dev/null +++ b/AcTools.ServerPlugin.DynamicConditions.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AcTools.ServerPlugin.DynamicConditions", "AcTools.ServerPlugin.DynamicConditions.csproj", "{3478E974-CE23-4CC7-A41B-2E868F12378F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3478E974-CE23-4CC7-A41B-2E868F12378F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3478E974-CE23-4CC7-A41B-2E868F12378F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3478E974-CE23-4CC7-A41B-2E868F12378F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3478E974-CE23-4CC7-A41B-2E868F12378F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Data/CommonAcConsts.cs b/Data/CommonAcConsts.cs new file mode 100644 index 0000000..2749890 --- /dev/null +++ b/Data/CommonAcConsts.cs @@ -0,0 +1,13 @@ +namespace AcTools.ServerPlugin.DynamicConditions.Data { + public static class CommonAcConsts { + /// + /// Seconds from 00:00. + /// + public static readonly int TimeMinimum = 8 * 60 * 60; + + /// + /// Seconds from 00:00. + /// + public static readonly int TimeMaximum = 18 * 60 * 60; + } +} \ No newline at end of file diff --git a/Data/Game.cs b/Data/Game.cs new file mode 100644 index 0000000..524b0dd --- /dev/null +++ b/Data/Game.cs @@ -0,0 +1,36 @@ +using System; + +namespace AcTools.ServerPlugin.DynamicConditions.Data { + public class Game { + public enum SessionType : byte { + Booking = 0, + Practice = 1, + Qualification = 2, + Race = 3, + Hotlap = 4, + TimeAttack = 5, + Drift = 6, + Drag = 7 + } + + public class ConditionProperties { + public static double GetRoadTemperature(double seconds, double ambientTemperature, double weatherCoefficient = 1.0) { + if (seconds < CommonAcConsts.TimeMinimum || seconds > CommonAcConsts.TimeMaximum) { + var minTemperature = GetRoadTemperature(CommonAcConsts.TimeMinimum, ambientTemperature, weatherCoefficient); + var maxTemperature = GetRoadTemperature(CommonAcConsts.TimeMaximum, ambientTemperature, weatherCoefficient); + var minValue = CommonAcConsts.TimeMinimum; + var maxValue = CommonAcConsts.TimeMaximum - 24 * 60 * 60; + if (seconds > CommonAcConsts.TimeMaximum) { + seconds -= 24 * 60 * 60; + } + + return minTemperature + (maxTemperature - minTemperature) * (seconds - minValue) / (maxValue - minValue); + } + + var time = (seconds / 60d / 60d - 7d) * 0.04167; + return ambientTemperature * (1d + 5.33332 * (weatherCoefficient == 0d ? 1d : weatherCoefficient) * (1d - time) * + (Math.Exp(-6d * time) * Math.Sin(6d * time) + 0.25) * Math.Sin(0.9 * time)); + } + } + } +} \ No newline at end of file diff --git a/Data/WeatherDescription.cs b/Data/WeatherDescription.cs new file mode 100644 index 0000000..bda3f09 --- /dev/null +++ b/Data/WeatherDescription.cs @@ -0,0 +1,33 @@ +namespace AcTools.ServerPlugin.DynamicConditions.Data { + public class WeatherDescription { + public WeatherType Type { get; } + + public double Temperature { get; } + + /// + /// Units: m/s. + /// + public double WindSpeed { get; } + + public double WindDirection { get; } + + /// + /// Units: percentage. + /// + public double Humidity { get; } + + /// + /// Units: hPa, aka 100 Pa. + /// + public double Pressure { get; } + + public WeatherDescription(WeatherType type, double temperature, double windSpeed, double windDirection, double humidity, double pressure) { + Type = type; + Temperature = temperature; + WindSpeed = windSpeed; + WindDirection = windDirection; + Humidity = humidity; + Pressure = pressure; + } + } +} diff --git a/Data/WeatherType.cs b/Data/WeatherType.cs new file mode 100644 index 0000000..b7bc5ed --- /dev/null +++ b/Data/WeatherType.cs @@ -0,0 +1,38 @@ +namespace AcTools.ServerPlugin.DynamicConditions.Data { + public enum WeatherType { + None = -1, + LightThunderstorm = 0, + Thunderstorm = 1, + HeavyThunderstorm = 2, + LightDrizzle = 3, + Drizzle = 4, + HeavyDrizzle = 5, + LightRain = 6, + Rain = 7, + HeavyRain = 8, + LightSnow = 9, + Snow = 10, + HeavySnow = 11, + LightSleet = 12, + Sleet = 13, + HeavySleet = 14, + Clear = 15, + FewClouds = 16, + ScatteredClouds = 17, + BrokenClouds = 18, + OvercastClouds = 19, + Fog = 20, + Mist = 21, + Smoke = 22, + Haze = 23, + Sand = 24, + Dust = 25, + Squalls = 26, + Tornado = 27, + Hurricane = 28, + Cold = 29, + Hot = 30, + Windy = 31, + Hail = 32 + } +} \ No newline at end of file diff --git a/LiveConditionsServerPlugin.cs b/LiveConditionsServerPlugin.cs new file mode 100644 index 0000000..6d64563 --- /dev/null +++ b/LiveConditionsServerPlugin.cs @@ -0,0 +1,532 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.CspCommands; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins.Messages; +using AcTools.ServerPlugin.DynamicConditions.Data; +using AcTools.ServerPlugin.DynamicConditions.Utils; +using JetBrains.Annotations; +using SystemHalf; + +namespace AcTools.ServerPlugin.DynamicConditions { + public class LiveConditionParams { + public bool UseV2; + public string ApiKey; + + public double TrackLatitude; + public double TrackLongitude; + public double TrackLengthKm; + public string TimezoneId; + + public bool UseRealConditions; + public TimeSpan TimeOffset; + public bool UseFixedStartingTime; + public int FixedStartingTimeValue = 12 * 60 * 60; + public DateTime FixedStartingDateValue = DateTime.Now; + public double TimeMultiplier = 1d; + public double TemperatureOffset; + public bool UseFixedAirTemperature; + public double FixedAirTemperature = 25d; + public TimeSpan WeatherTypeChangePeriod = TimeSpan.FromMinutes(5d); + public bool WeatherTypeChangeToNeighboursOnly = true; + public double WeatherRainChance = 0.05; + public double WeatherThunderChance = 0.005; + public double TrackGripStartingValue = 99d; + public double TrackGripIncreasePerLap = 0.05d; + public double TrackGripTransfer = 80d; + + public double RainTimeMultiplier = 1d; + public TimeSpan RainWetnessIncreaseTime = TimeSpan.FromMinutes(3d); + public TimeSpan RainWetnessDecreaseTime = TimeSpan.FromMinutes(15d); + public TimeSpan RainWaterIncreaseTime = TimeSpan.FromMinutes(30d); + public TimeSpan RainWaterDecreaseTime = TimeSpan.FromMinutes(120d); + } + + public class LiveConditionsServerPlugin : AcServerPlugin { + private static readonly TimeSpan UpdateRainPeriod = TimeSpan.FromSeconds(0.5d); + private static readonly TimeSpan UpdateWeatherPeriod = TimeSpan.FromMinutes(10d); + + private readonly LiveConditionParams _liveParams; + private TimeSpan _updatePeriod; + private TimeSpan _broadcastPeriod; + private double _drivenLapsEstimate; + private double _lapLengthKm; + private bool _disposed; + + public LiveConditionsServerPlugin(LiveConditionParams liveParams) { + _liveParams = liveParams; + _lapLengthKm = Math.Max(1, liveParams.TrackLengthKm); + _startingDate = DateTime.Now; + _broadcastPeriod = liveParams.UseRealConditions ? TimeSpan.FromMinutes(1d) + : TimeSpan.FromMinutes((liveParams.WeatherTypeChangePeriod.TotalMinutes * 0.2).Clamp(0.1, 2d)); + _startingDate = _liveParams.UseFixedStartingTime + ? _liveParams.FixedStartingDateValue.Date + TimeSpan.FromSeconds(_liveParams.FixedStartingTimeValue) + - GetTimezoneOffset() + : DateTime.Now + _liveParams.TimeOffset; + SyncWeatherAsync().Ignore(); + UpdateStateAsync().Ignore(); + } + + private static void LogMessage(string msg) { + Logging.Write(msg); + } + + private TimeSpan GetTimezoneOffset() { + var utcOffset = new DateTimeOffset(DateTime.UtcNow, TimeSpan.Zero); + return TimeZoneInfo.FindSystemTimeZoneById(_liveParams.TimezoneId).GetUtcOffset(utcOffset) - TimeZoneInfo.Local.GetUtcOffset(utcOffset); + } + + public override void OnInit() { + base.OnInit(); + BroadcastLoopAsync().Ignore(); + } + + public override void Dispose() { + _disposed = true; + } + + public override void OnNewSession(MsgSessionInfo msg) { + base.OnNewSession(msg); + _drivenLapsEstimate *= (_liveParams.TrackGripTransfer / 100d).Saturate(); + } + + private string GetSerializedCspCommand([NotNull] WeatherDescription current, [NotNull] WeatherDescription next) { + var transition = GetWeatherTransition(); + var date = _startingDate + TimeSpan.FromSeconds(_timePassedTotal.Elapsed.TotalSeconds * _liveParams.TimeMultiplier); + var ambientTemperature = _liveParams.UseFixedAirTemperature + ? _liveParams.FixedAirTemperature : transition.Lerp(current.Temperature, next.Temperature) + _liveParams.TemperatureOffset; + var roadTemperature = Game.ConditionProperties.GetRoadTemperature(date.TimeOfDay.TotalSeconds.RoundToInt(), ambientTemperature, + transition.Lerp(GetRoadTemperatureCoefficient(current.Type), GetRoadTemperatureCoefficient(next.Type))); + var windSpeed = transition.Lerp(current.WindSpeed, next.WindSpeed) * 3.6; + var grip = ((_liveParams.TrackGripStartingValue + _liveParams.TrackGripIncreasePerLap * _drivenLapsEstimate) / 100d).Clamp(0.6, 1d); + + // Second version would affect conditions physics, so it’s optional, active only if server requires CSP v1643 or newer + return _liveParams.UseV2 + ? new CommandWeatherSetV2 { + Timestamp = (ulong)date.ToUnixTimestamp(), + TimeToApply = (Half)_broadcastPeriod.TotalSeconds, + WeatherCurrent = (CommandWeatherType)current.Type, + WeatherNext = (CommandWeatherType)next.Type, + Transition = (ushort)(65535 * transition), + WindSpeedKmh = (Half)windSpeed, + WindDirectionDeg = (Half)transition.Lerp(current.WindDirection, next.WindDirection), + Humidity = (byte)(transition.Lerp(current.Humidity, next.Humidity) * 255d), + Pressure = (Half)transition.Lerp(current.Pressure, next.Pressure), + TemperatureAmbient = (Half)ambientTemperature, + TemperatureRoad = (Half)roadTemperature, + TrackGrip = grip, + RainIntensity = (Half)_rainIntensity, + RainWetness = (Half)_rainWetness, + RainWater = (Half)_rainLag[_rainCursor] + }.Serialize() + : new CommandWeatherSetV1 { + Timestamp = (ulong)date.ToUnixTimestamp(), + TimeToApply = (float)_broadcastPeriod.TotalSeconds, + WeatherCurrent = (CommandWeatherType)current.Type, + WeatherNext = (CommandWeatherType)next.Type, + Transition = (float)transition, + }.Serialize(); + } + + public override void OnClientLoaded(MsgClientLoaded msg) { + // Sending conditions directly to all newly connected: + if (_weatherCurrent != null && _weatherNext != null) { + PluginManager.SendCspCommand(msg.CarId, GetSerializedCspCommand(_weatherCurrent, _weatherNext)); + } + } + + private async Task BroadcastLoopAsync() { + while (!_disposed) { + if (_weatherCurrent != null && _weatherNext != null && PluginManager.IsConnected) { + PluginManager.BroadcastCspCommand(GetSerializedCspCommand(_weatherCurrent, _weatherNext)); + } + await Task.Delay(_broadcastPeriod); + } + } + + private readonly Stopwatch _timePassedTotal = Stopwatch.StartNew(); + private readonly Stopwatch _weatherTransitionStopwatch = Stopwatch.StartNew(); + + [CanBeNull] + private WeatherDescription _weatherCurrent, _weatherNext; + + private double _rainCurrent, _rainNext; + + private DateTime _startingDate; + private double _rainIntensity; + private double _rainWetness; + private double _rainWater; + + private int _rainCursor; + private double[] _rainLag = new double[120]; // two updates per second → 1 minute for changes to apply + + private double GetWeatherTransition() { + return (_weatherTransitionStopwatch.Elapsed.TotalSeconds / _updatePeriod.TotalSeconds).Saturate().SmoothStep(); + } + + private async Task UpdateStateAsync() { + while (!_disposed) { + var transition = GetWeatherTransition(); + + var current = _weatherCurrent; + var next = _weatherNext; + var weatherMissing = current == null || next == null; + _rainIntensity = weatherMissing ? 0d : transition.Lerp(_rainCurrent, _rainNext); + + var date = _startingDate + TimeSpan.FromSeconds(_timePassedTotal.Elapsed.TotalSeconds * _liveParams.TimeMultiplier); + var ambientTemperature = weatherMissing ? 25d : _liveParams.UseFixedAirTemperature + ? _liveParams.FixedAirTemperature : transition.Lerp(current.Temperature, next.Temperature) + _liveParams.TemperatureOffset; + var roadTemperature = Game.ConditionProperties.GetRoadTemperature(date.TimeOfDay.TotalSeconds.RoundToInt(), ambientTemperature, + transition.Lerp( + GetRoadTemperatureCoefficient(current?.Type ?? WeatherType.Clear), + GetRoadTemperatureCoefficient(next?.Type ?? WeatherType.Clear))); + + if (_rainIntensity > 0d) { + _rainWetness = Math.Min(1d, _rainWetness + _rainIntensity.Lerp(0.3, 1.7d) + * UpdateRainPeriod.TotalSeconds / Math.Max(1d, _liveParams.RainWetnessIncreaseTime.TotalSeconds * _liveParams.RainTimeMultiplier)); + } else { + _rainWetness = Math.Max(0d, _rainWetness - roadTemperature.LerpInvSat(10d, 35d).Lerp(0.3, 1.7d) + * UpdateRainPeriod.TotalSeconds / Math.Max(1d, _liveParams.RainWetnessDecreaseTime.TotalSeconds * _liveParams.RainTimeMultiplier)); + } + + if (_rainWater < _rainIntensity) { + _rainWater = Math.Min(_rainIntensity, _rainWater + _rainIntensity.Lerp(0.3, 1.7d) + * UpdateRainPeriod.TotalSeconds / Math.Max(1d, _liveParams.RainWaterIncreaseTime.TotalSeconds * _liveParams.RainTimeMultiplier)); + } else { + _rainWater = Math.Max(_rainIntensity, _rainWater - roadTemperature.LerpInvSat(10d, 35d).Lerp(0.3, 1.7d) + * UpdateRainPeriod.TotalSeconds / Math.Max(1d, _liveParams.RainWaterDecreaseTime.TotalSeconds * _liveParams.RainTimeMultiplier)); + } + + if (PluginManager != null) { + foreach (var info in PluginManager.GetDriverInfos()) { + if (info?.IsConnected == true) { + var drivenDistanceKm = info.CurrentSpeed * UpdateRainPeriod.TotalHours; + _drivenLapsEstimate += drivenDistanceKm / _lapLengthKm; + } + } + } + + _rainLag[_rainCursor] = _rainWater; + if (++_rainCursor == _rainLag.Length) { + _rainCursor = 0; + } + + await Task.Delay(UpdateRainPeriod); + } + } + + private void ApplyWeather([CanBeNull] WeatherDescription weather) { + LogMessage("New weather type: " + weather?.Type); + if (weather == null) { + return; + } + + var rain = GetRainIntensity(weather.Type) * MathUtils.Random(0.8, 1.2); + _weatherTransitionStopwatch.Restart(); + _rainCurrent = _weatherNext != null ? _rainNext : rain; + _rainNext = rain; + _weatherCurrent = _weatherNext ?? weather; + _weatherNext = weather; + } + + private static readonly Dictionary> Neighbours = new Dictionary>(); + + static LiveConditionsServerPlugin() { + void AddConnection(WeatherType a, WeatherType b) { + if (!Neighbours.ContainsKey(a)) Neighbours[a] = new List(); + if (!Neighbours.ContainsKey(b)) Neighbours[b] = new List(); + Neighbours[a].Add(b); + Neighbours[b].Add(a); + } + + AddConnection(WeatherType.Clear, WeatherType.FewClouds); + AddConnection(WeatherType.Clear, WeatherType.ScatteredClouds); + AddConnection(WeatherType.Clear, WeatherType.Mist); + AddConnection(WeatherType.FewClouds, WeatherType.ScatteredClouds); + AddConnection(WeatherType.FewClouds, WeatherType.BrokenClouds); + AddConnection(WeatherType.FewClouds, WeatherType.Mist); + AddConnection(WeatherType.FewClouds, WeatherType.Windy); + AddConnection(WeatherType.FewClouds, WeatherType.LightDrizzle); + AddConnection(WeatherType.ScatteredClouds, WeatherType.BrokenClouds); + AddConnection(WeatherType.ScatteredClouds, WeatherType.OvercastClouds); + AddConnection(WeatherType.ScatteredClouds, WeatherType.Mist); + AddConnection(WeatherType.ScatteredClouds, WeatherType.Windy); + AddConnection(WeatherType.ScatteredClouds, WeatherType.Drizzle); + AddConnection(WeatherType.ScatteredClouds, WeatherType.LightRain); + AddConnection(WeatherType.BrokenClouds, WeatherType.OvercastClouds); + AddConnection(WeatherType.BrokenClouds, WeatherType.Mist); + AddConnection(WeatherType.BrokenClouds, WeatherType.Windy); + AddConnection(WeatherType.BrokenClouds, WeatherType.Drizzle); + AddConnection(WeatherType.BrokenClouds, WeatherType.Rain); + AddConnection(WeatherType.OvercastClouds, WeatherType.Mist); + AddConnection(WeatherType.OvercastClouds, WeatherType.Fog); + AddConnection(WeatherType.OvercastClouds, WeatherType.Windy); + AddConnection(WeatherType.OvercastClouds, WeatherType.HeavyDrizzle); + AddConnection(WeatherType.OvercastClouds, WeatherType.Rain); + AddConnection(WeatherType.Fog, WeatherType.Mist); + AddConnection(WeatherType.Fog, WeatherType.Rain); + AddConnection(WeatherType.Mist, WeatherType.LightDrizzle); + AddConnection(WeatherType.Mist, WeatherType.LightRain); + AddConnection(WeatherType.Mist, WeatherType.Windy); + AddConnection(WeatherType.Windy, WeatherType.Drizzle); + AddConnection(WeatherType.Windy, WeatherType.LightRain); + AddConnection(WeatherType.Windy, WeatherType.Rain); + AddConnection(WeatherType.LightDrizzle, WeatherType.Drizzle); + AddConnection(WeatherType.LightDrizzle, WeatherType.HeavyDrizzle); + AddConnection(WeatherType.LightDrizzle, WeatherType.LightRain); + AddConnection(WeatherType.LightDrizzle, WeatherType.Rain); + AddConnection(WeatherType.Drizzle, WeatherType.HeavyDrizzle); + AddConnection(WeatherType.Drizzle, WeatherType.LightRain); + AddConnection(WeatherType.Drizzle, WeatherType.Rain); + AddConnection(WeatherType.HeavyDrizzle, WeatherType.LightRain); + AddConnection(WeatherType.HeavyDrizzle, WeatherType.Rain); + AddConnection(WeatherType.HeavyDrizzle, WeatherType.HeavyRain); + AddConnection(WeatherType.HeavyDrizzle, WeatherType.LightThunderstorm); + AddConnection(WeatherType.LightRain, WeatherType.Rain); + AddConnection(WeatherType.LightRain, WeatherType.LightThunderstorm); + AddConnection(WeatherType.Rain, WeatherType.HeavyRain); + AddConnection(WeatherType.Rain, WeatherType.Thunderstorm); + AddConnection(WeatherType.HeavyRain, WeatherType.Thunderstorm); + AddConnection(WeatherType.HeavyRain, WeatherType.HeavyThunderstorm); + AddConnection(WeatherType.LightThunderstorm, WeatherType.Thunderstorm); + AddConnection(WeatherType.Thunderstorm, WeatherType.HeavyThunderstorm); + + foreach (var neighbour in Neighbours) { + neighbour.Value.Add(neighbour.Key); + } + } + + private static readonly List> ChancesRegular = new List> { + Tuple.Create(0.4, WeatherType.Clear), + Tuple.Create(0.2, WeatherType.FewClouds), + Tuple.Create(0.1, WeatherType.ScatteredClouds), + Tuple.Create(0.1, WeatherType.BrokenClouds), + Tuple.Create(0.2, WeatherType.OvercastClouds), + Tuple.Create(0.1, WeatherType.Fog), + Tuple.Create(0.1, WeatherType.Mist), + Tuple.Create(0.2, WeatherType.Windy), + }; + + private static readonly List> ChancesRain = new List> { + Tuple.Create(0.1, WeatherType.LightDrizzle), + Tuple.Create(0.2, WeatherType.Drizzle), + Tuple.Create(0.2, WeatherType.HeavyDrizzle), + Tuple.Create(0.2, WeatherType.LightRain), + Tuple.Create(0.3, WeatherType.Rain), + Tuple.Create(0.1, WeatherType.HeavyRain), + }; + + private static readonly List> ChancesThunderstorm = new List> { + Tuple.Create(0.6, WeatherType.LightThunderstorm), + Tuple.Create(0.3, WeatherType.Thunderstorm), + Tuple.Create(0.1, WeatherType.HeavyThunderstorm), + }; + + private WeatherType? PickRandomWeatherType(List> table, [CanBeNull] List allowed) { + if (allowed != null) { + table = table.Where(x => allowed.Contains(x.Item2)).ToList(); + } + if (table.Count == 0) return null; + var chance = MathUtils.Random() * table.Sum(x => x.Item1); + foreach (var item in table) { + if (chance < item.Item1) return item.Item2; + chance -= item.Item1; + } + return table.FirstOrDefault()?.Item2; + } + + private WeatherType GenerateRandomWeatherType(WeatherType? currentType) { + if (!_liveParams.WeatherTypeChangeToNeighboursOnly && currentType.HasValue && MathUtils.Random() < 0.2) { + return currentType.Value; + } + + List allowed = null; + if (currentType.HasValue && _liveParams.WeatherTypeChangeToNeighboursOnly) { + Neighbours.TryGetValue(currentType.Value, out allowed); + } + + var chance = MathUtils.Random() * Math.Max(1, _liveParams.WeatherThunderChance + _liveParams.WeatherRainChance) - _liveParams.WeatherThunderChance; + if (chance < 0 || MathUtils.Random() > 0.2 && ChancesThunderstorm.Any(x => x.Item2 == currentType)) { + var type = PickRandomWeatherType(ChancesThunderstorm, allowed); + if (type.HasValue) { + return type.Value; + } + } + if (chance < _liveParams.WeatherRainChance || MathUtils.Random() > 0.2 && ChancesRain.Any(x => x.Item2 == currentType)) { + var type = PickRandomWeatherType(ChancesRain, allowed); + if (type.HasValue) { + return type.Value; + } + } + return PickRandomWeatherType(ChancesRegular, allowed) ?? WeatherType.Clear; + } + + private Tuple EstimateTemperatureWindSpeedHumidity(WeatherType type) { + if (type == WeatherType.Clear) return Tuple.Create(26d, 1d, 0.6); + if (type == WeatherType.FewClouds) return Tuple.Create(25d, 2d, 0.6); + if (type == WeatherType.ScatteredClouds) return Tuple.Create(25d, 3d, 0.6); + if (type == WeatherType.BrokenClouds) return Tuple.Create(25d, 4d, 0.6); + if (type == WeatherType.OvercastClouds) return Tuple.Create(24d, 5d, 0.6); + if (type == WeatherType.Fog) return Tuple.Create(24d, 0d, 0.9); + if (type == WeatherType.Mist) return Tuple.Create(24d, 0d, 0.8); + if (type == WeatherType.Windy) return Tuple.Create(24d, 10d, 0.4); + if (type == WeatherType.LightDrizzle) return Tuple.Create(25d, 2d, 0.7); + if (type == WeatherType.Drizzle) return Tuple.Create(24d, 3d, 0.7); + if (type == WeatherType.HeavyDrizzle) return Tuple.Create(23d, 4d, 0.7); + if (type == WeatherType.LightRain) return Tuple.Create(24d, 4d, 0.8); + if (type == WeatherType.Rain) return Tuple.Create(23d, 6d, 0.8); + if (type == WeatherType.HeavyRain) return Tuple.Create(23d, 10d, 0.8); + if (type == WeatherType.LightThunderstorm) return Tuple.Create(23d, 12d, 0.9); + if (type == WeatherType.Thunderstorm) return Tuple.Create(22d, 13d, 0.9); + if (type == WeatherType.HeavyThunderstorm) return Tuple.Create(22d, 14d, 0.9); + return Tuple.Create(24d, 1d, 0.6); + } + + private WeatherDescription GenerateRandomDescription(WeatherType? currentType) { + var type = GenerateRandomWeatherType(currentType); + Logging.Debug($"Switching from {currentType?.ToString() ?? "?"} to {type}"); + var estimated = EstimateTemperatureWindSpeedHumidity(type); + return new WeatherDescription(type, estimated.Item1 * MathUtils.Random(0.95, 1.05), estimated.Item2 * MathUtils.Random(0.6, 1.2), + MathUtils.Random() * 360, estimated.Item3 * MathUtils.Random(0.6, 1.2), 1030); + } + + private async Task SyncWeatherAsync() { + if (_liveParams.UseRealConditions) { + _updatePeriod = UpdateWeatherPeriod; + while (!_disposed) { + ApplyWeather(await OpenWeatherApiProvider.GetWeatherAsync(_liveParams.ApiKey, _liveParams.TrackLatitude, _liveParams.TrackLongitude)); + await Task.Delay(UpdateWeatherPeriod); + } + } else { + while (!_disposed) { + _updatePeriod = TimeSpan.FromSeconds(_liveParams.WeatherTypeChangePeriod.TotalSeconds * MathUtils.Random(0.5, 1.5)); + ApplyWeather(GenerateRandomDescription(_weatherNext?.Type)); + await Task.Delay(_updatePeriod); + } + } + } + + private static double GetRainIntensity(WeatherType type) { + switch (type) { + case WeatherType.None: + return 0d; + case WeatherType.LightThunderstorm: + return 0.5; + case WeatherType.Thunderstorm: + return 0.6; + case WeatherType.HeavyThunderstorm: + return 0.7; + case WeatherType.LightDrizzle: + return 0.1; + case WeatherType.Drizzle: + return 0.2; + case WeatherType.HeavyDrizzle: + return 0.3; + case WeatherType.LightRain: + return 0.3; + case WeatherType.Rain: + return 0.4; + case WeatherType.HeavyRain: + return 0.5; + case WeatherType.LightSnow: + return 0.2; + case WeatherType.Snow: + return 0.3; + case WeatherType.HeavySnow: + return 0.4; + case WeatherType.LightSleet: + return 0.3; + case WeatherType.Sleet: + return 0.4; + case WeatherType.HeavySleet: + return 0.5; + case WeatherType.Squalls: + return 0.6; + case WeatherType.Tornado: + return 0.5; + case WeatherType.Hurricane: + return 0.6; + default: + return 0d; + } + } + + private static double GetRoadTemperatureCoefficient(WeatherType type) { + switch (type) { + case WeatherType.None: + return 1d; + case WeatherType.LightThunderstorm: + return 0.7; + case WeatherType.Thunderstorm: + return 0.2; + case WeatherType.HeavyThunderstorm: + return -0.2; + case WeatherType.LightDrizzle: + return 0.1; + case WeatherType.Drizzle: + return -0.1; + case WeatherType.HeavyDrizzle: + return -0.3; + case WeatherType.LightRain: + return 0.01; + case WeatherType.Rain: + return -0.2; + case WeatherType.HeavyRain: + return -0.5; + case WeatherType.LightSnow: + return -0.7; + case WeatherType.Snow: + return -0.8; + case WeatherType.HeavySnow: + return -0.9; + case WeatherType.LightSleet: + return -1d; + case WeatherType.Sleet: + return -1d; + case WeatherType.HeavySleet: + return -1d; + case WeatherType.Squalls: + return -0.5; + case WeatherType.Tornado: + return -0.3; + case WeatherType.Hurricane: + return -0.6; + case WeatherType.Clear: + return 1d; + case WeatherType.FewClouds: + return 1d; + case WeatherType.ScatteredClouds: + return 0.8; + case WeatherType.BrokenClouds: + return 0.1; + case WeatherType.OvercastClouds: + return 0.01; + case WeatherType.Fog: + return -0.3; + case WeatherType.Mist: + return -0.2; + case WeatherType.Smoke: + return -0.2; + case WeatherType.Haze: + return 0.9; + case WeatherType.Sand: + return 1d; + case WeatherType.Dust: + return 1d; + case WeatherType.Cold: + return -0.8; + case WeatherType.Hot: + return 1d; + case WeatherType.Windy: + return 0.3; + case WeatherType.Hail: + return -1d; + default: + return 0d; + } + } + } +} \ No newline at end of file diff --git a/OpenWeatherApiProvider.cs b/OpenWeatherApiProvider.cs new file mode 100644 index 0000000..8db7e03 --- /dev/null +++ b/OpenWeatherApiProvider.cs @@ -0,0 +1,270 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Xml.Linq; +using AcTools.ServerPlugin.DynamicConditions.Data; +using AcTools.ServerPlugin.DynamicConditions.Utils; +using JetBrains.Annotations; + +namespace AcTools.ServerPlugin.DynamicConditions { + public static class OpenWeatherApiProvider { + public enum OpenWeatherType { + ThunderstormWithLightRain = 200, + ThunderstormWithRain = 201, + ThunderstormWithHeavyRain = 202, + LightThunderstorm = 210, + Thunderstorm = 211, + HeavyThunderstorm = 212, + RaggedThunderstorm = 221, + ThunderstormWithLightDrizzle = 230, + ThunderstormWithDrizzle = 231, + ThunderstormWithHeavyDrizzle = 232, + LightIntensityDrizzle = 300, + Drizzle = 301, + HeavyIntensityDrizzle = 302, + LightIntensityDrizzleRain = 310, + DrizzleRain = 311, + HeavyIntensityDrizzleRain = 312, + ShowerRainAndDrizzle = 313, + HeavyShowerRainAndDrizzle = 314, + ShowerDrizzle = 321, + LightRain = 500, + ModerateRain = 501, + HeavyIntensityRain = 502, + VeryHeavyRain = 503, + ExtremeRain = 504, + FreezingRain = 511, + LightIntensityShowerRain = 520, + ShowerRain = 521, + HeavyIntensityShowerRain = 522, + RaggedShowerRain = 531, + LightSnow = 600, + Snow = 601, + HeavySnow = 602, + Sleet = 611, + ShowerSleet = 612, + LightRainAndSnow = 615, + RainAndSnow = 616, + LightShowerSnow = 620, + ShowerSnow = 621, + HeavyShowerSnow = 622, + Mist = 701, + Smoke = 711, + Haze = 721, + SandAndDustWhirls = 731, + Fog = 741, + Sand = 751, + Dust = 761, + VolcanicAsh = 762, + Squalls = 771, + Tornado = 781, + ClearSky = 800, + FewClouds = 801, + ScatteredClouds = 802, + BrokenClouds = 803, + OvercastClouds = 804, + TornadoExtreme = 900, + TropicalStorm = 901, + Hurricane = 902, + Cold = 903, + Hot = 904, + Windy = 905, + Hail = 906, + Calm = 951, + LightBreeze = 952, + GentleBreeze = 953, + ModerateBreeze = 954, + FreshBreeze = 955, + StrongBreeze = 956, + HighWind = 957, + Gale = 958, + SevereGale = 959, + Storm = 960, + ViolentStorm = 961, + HurricaneAdditional = 962, + } + + private static WeatherType OpenWeatherTypeToCommonType(OpenWeatherType type) { + switch (type) { + case OpenWeatherType.RaggedThunderstorm: + case OpenWeatherType.Thunderstorm: + case OpenWeatherType.ThunderstormWithLightRain: + case OpenWeatherType.ThunderstormWithRain: + case OpenWeatherType.ThunderstormWithHeavyRain: + case OpenWeatherType.ThunderstormWithLightDrizzle: + case OpenWeatherType.ThunderstormWithDrizzle: + case OpenWeatherType.ThunderstormWithHeavyDrizzle: + return WeatherType.Thunderstorm; + + case OpenWeatherType.LightThunderstorm: + return WeatherType.LightThunderstorm; + + case OpenWeatherType.HeavyThunderstorm: + case OpenWeatherType.TropicalStorm: + return WeatherType.HeavyThunderstorm; + + case OpenWeatherType.LightIntensityDrizzle: + case OpenWeatherType.LightIntensityDrizzleRain: + return WeatherType.LightDrizzle; + + case OpenWeatherType.Drizzle: + case OpenWeatherType.DrizzleRain: + case OpenWeatherType.ShowerDrizzle: + return WeatherType.Drizzle; + + case OpenWeatherType.HeavyIntensityDrizzle: + case OpenWeatherType.HeavyIntensityDrizzleRain: + return WeatherType.HeavyDrizzle; + + case OpenWeatherType.LightRain: + case OpenWeatherType.LightIntensityShowerRain: + return WeatherType.LightRain; + + case OpenWeatherType.ModerateRain: + case OpenWeatherType.FreezingRain: + case OpenWeatherType.ShowerRainAndDrizzle: + case OpenWeatherType.ShowerRain: + case OpenWeatherType.RaggedShowerRain: + return WeatherType.Rain; + + case OpenWeatherType.HeavyIntensityRain: + case OpenWeatherType.VeryHeavyRain: + case OpenWeatherType.ExtremeRain: + case OpenWeatherType.HeavyShowerRainAndDrizzle: + case OpenWeatherType.HeavyIntensityShowerRain: + return WeatherType.HeavyRain; + + case OpenWeatherType.LightSnow: + case OpenWeatherType.LightShowerSnow: + return WeatherType.LightSnow; + + case OpenWeatherType.Snow: + case OpenWeatherType.ShowerSnow: + return WeatherType.Snow; + + case OpenWeatherType.HeavySnow: + case OpenWeatherType.HeavyShowerSnow: + return WeatherType.HeavySnow; + + case OpenWeatherType.LightRainAndSnow: + return WeatherType.LightSleet; + + case OpenWeatherType.RainAndSnow: + case OpenWeatherType.Sleet: + return WeatherType.Sleet; + + case OpenWeatherType.ShowerSleet: + return WeatherType.HeavySleet; + + case OpenWeatherType.Mist: + return WeatherType.Mist; + + case OpenWeatherType.Smoke: + return WeatherType.Smoke; + + case OpenWeatherType.Haze: + return WeatherType.Haze; + + case OpenWeatherType.Sand: + case OpenWeatherType.SandAndDustWhirls: + return WeatherType.Sand; + + case OpenWeatherType.Dust: + case OpenWeatherType.VolcanicAsh: + return WeatherType.Dust; + + case OpenWeatherType.Fog: + return WeatherType.Fog; + + case OpenWeatherType.Squalls: + return WeatherType.Squalls; + + case OpenWeatherType.Tornado: + case OpenWeatherType.TornadoExtreme: + return WeatherType.Tornado; + + case OpenWeatherType.ClearSky: + case OpenWeatherType.Calm: + case OpenWeatherType.LightBreeze: + return WeatherType.Clear; + + case OpenWeatherType.FewClouds: + case OpenWeatherType.GentleBreeze: + case OpenWeatherType.ModerateBreeze: + return WeatherType.FewClouds; + + case OpenWeatherType.ScatteredClouds: + return WeatherType.ScatteredClouds; + + case OpenWeatherType.BrokenClouds: + return WeatherType.BrokenClouds; + + case OpenWeatherType.OvercastClouds: + return WeatherType.OvercastClouds; + + case OpenWeatherType.Hurricane: + case OpenWeatherType.Gale: + case OpenWeatherType.SevereGale: + case OpenWeatherType.Storm: + case OpenWeatherType.ViolentStorm: + case OpenWeatherType.HurricaneAdditional: + return WeatherType.Hurricane; + + case OpenWeatherType.Cold: + return WeatherType.Cold; + + case OpenWeatherType.Hot: + return WeatherType.Hot; + + case OpenWeatherType.Windy: + case OpenWeatherType.FreshBreeze: + case OpenWeatherType.StrongBreeze: + case OpenWeatherType.HighWind: + return WeatherType.Windy; + + case OpenWeatherType.Hail: + return WeatherType.Hail; + + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + private const string RequestWeatherUri = "http://api.openweathermap.org/data/2.5/weather?lat={0}&lon={1}&APPID={2}&mode=xml&units=metric"; + + private static double? TryParse(string s) { + return double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : (double?)null; + } + + [ItemNotNull] + public static async Task GetWeatherAsync(string apiKey, double latitude, double longitude) { + var requestUri = string.Format(RequestWeatherUri, latitude, longitude, apiKey); + using (var client = new WebClient()) { + var data = await client.DownloadStringTaskAsync(requestUri); + + try { + var doc = XDocument.Parse(data); + var temperatureValue = doc.Descendants(@"temperature").FirstOrDefault()?.Attribute(@"value")?.Value; + var weatherNode = doc.Descendants(@"weather").FirstOrDefault(); + var windNode = doc.Descendants(@"wind").FirstOrDefault(); + if (temperatureValue == null || weatherNode == null || windNode == null) throw new Exception("Invalid response"); + + var type = OpenWeatherTypeToCommonType((OpenWeatherType)(TryParse(weatherNode.Attribute(@"number")?.Value) ?? 0)); + var temperature = TryParse(temperatureValue) ?? 25d; + + var windSpeed = TryParse(windNode.Descendants(@"speed").First().Attribute("value")?.Value) ?? 0d; + var windDirection = TryParse(windNode.Descendants(@"direction").First().Attribute("value")?.Value) ?? 0d; + + var humidity = TryParse(doc.Descendants(@"humidity").FirstOrDefault()?.Attribute(@"value")?.Value) ?? 70d; + var pressure = TryParse(doc.Descendants(@"pressure").FirstOrDefault()?.Attribute(@"value")?.Value) ?? 1000d; + return new WeatherDescription(type, temperature, windSpeed, windDirection, humidity, pressure); + } catch { + Logging.Warning("Failed to parse: " + data); + throw; + } + } + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..678c0a0 --- /dev/null +++ b/Program.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins; +using AcTools.ServerPlugin.DynamicConditions.Utils; + +namespace AcTools.ServerPlugin.DynamicConditions { + internal class Program { + public static int Main(string[] args) { + var configs = GetConfigs(args); + if (configs.Length == 0) { + Console.Error.WriteLine("Usage: AcTools.ServerPlugin.DynamicConditions.exe ..."); + return 1; + } + + try { + var managers = configs.Select(arg => { + var programParams = ProgramParams.GetParams(arg); + var manager = new AcServerPluginManager(programParams.Plugin); + foreach (var entry in programParams.ExternalPlugins) { + manager.AddExternalPlugin(entry); + } + + manager.AddPlugin(new LiveConditionsServerPlugin(programParams.Weather)); + manager.Connect(); + return manager; + }).ToList(); + + Console.WriteLine(managers.Count == 1 + ? "> Server plugin is running. Press to close." + : $"> {managers.Count} server plugins are running. Press to close."); + Console.ReadLine(); + return 0; + } catch (Exception e) { + Console.Error.WriteLine(e.ToString()); + return 1; + } + } + + private static string GetEmbeddedConfig() { + using (var stream = File.Open(MainExecutingFile.Location, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = new BinaryReader(stream, Encoding.Default, true)) { + reader.BaseStream.Seek(-8, SeekOrigin.End); + if (reader.ReadUInt32() == 0xBEE5) { + var length = reader.ReadInt32(); + if (length > 0 && length < 1e6) { + reader.BaseStream.Seek(-length - 8, SeekOrigin.Current); + return Encoding.UTF8.GetString(reader.ReadBytes(length)); + } + } + } + return null; + } + + private static string[][] GetConfigs(string[] args) { + var baked = GetEmbeddedConfig(); + return baked != null ? new[]{ baked.Split('\n') } : args.Select(File.ReadAllLines).ToArray(); + } + } +} \ No newline at end of file diff --git a/ProgramParams.cs b/ProgramParams.cs new file mode 100644 index 0000000..5361df0 --- /dev/null +++ b/ProgramParams.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using AcTools.ServerPlugin.DynamicConditions.AcPlugins; + +namespace AcTools.ServerPlugin.DynamicConditions { + public class ProgramParams { + public AcServerPluginManagerSettings Plugin; + public List ExternalPlugins; + public LiveConditionParams Weather; + + public static ProgramParams GetParams(string[] data) { + var ret = new ProgramParams { + Plugin = new AcServerPluginManagerSettings { + LogServerRequests = false + }, + ExternalPlugins = new List(), + Weather = new LiveConditionParams() + }; + + var keys = new Dictionary> { + ["message"] = Console.WriteLine, + ["plugin.listeningPort"] = x => ret.Plugin.ListeningPort = int.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["plugin.remotePort"] = x => ret.Plugin.RemotePort = int.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["plugin.remoteHostName"] = x => ret.Plugin.RemoteHostName = x, + ["plugin.serverCapacity"] = x => ret.Plugin.Capacity = int.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["plugin.externalPlugin"] = x => { + var p = x.Split(','); + if (p.Length != 2) throw new Exception("Format for plugin.externalPlugin: , :"); + var r = p[1].Split(':'); + if (r.Length != 2) throw new Exception("Format for plugin.externalPlugin: , :"); + ret.ExternalPlugins.Add(new ExternalPluginInfo( + int.Parse(p[0].Trim(), NumberStyles.Any, CultureInfo.InvariantCulture), + r[0].Trim(), + int.Parse(r[1], NumberStyles.Any, CultureInfo.InvariantCulture))); + }, + ["weather.useV2"] = x => ret.Weather.UseV2 = x != "0", + ["weather.apiKey"] = x => ret.Weather.ApiKey = x, + ["weather.trackLatitude"] = x => ret.Weather.TrackLatitude = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.trackLongitude"] = x => ret.Weather.TrackLongitude = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.trackLengthKm"] = x => ret.Weather.TrackLengthKm = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.trackTimezoneId"] = x => ret.Weather.TimezoneId = x, + ["weather.useRealConditions"] = x => ret.Weather.UseRealConditions = x != "0", + ["weather.timeOffset"] = x => ret.Weather.TimeOffset = TimeSpan.Parse(x, CultureInfo.InvariantCulture), + ["weather.useFixedStartingTime"] = x => ret.Weather.UseFixedStartingTime = x != "0", + ["weather.fixedStartingTimeValue"] = x => ret.Weather.FixedStartingTimeValue = int.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.fixedStartingDateValue"] = x => ret.Weather.FixedStartingDateValue = DateTime.Parse(x, CultureInfo.InvariantCulture), + ["weather.timeMultiplier"] = x => ret.Weather.TimeMultiplier = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.temperatureOffset"] = x => ret.Weather.TemperatureOffset = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.useFixedAirTemperature"] = x => ret.Weather.UseFixedAirTemperature = x != "0", + ["weather.fixedAirTemperature"] = x => ret.Weather.FixedAirTemperature = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.weatherTypeChangePeriod"] = x => ret.Weather.WeatherTypeChangePeriod = TimeSpan.Parse(x, CultureInfo.InvariantCulture), + ["weather.weatherTypeChangeToNeighboursOnly"] = x => ret.Weather.WeatherTypeChangeToNeighboursOnly = x != "0", + ["weather.weatherRainChance"] = x => ret.Weather.WeatherRainChance = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.weatherThunderChance"] = x => ret.Weather.WeatherThunderChance = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.trackGripStartingValue"] = x => ret.Weather.TrackGripStartingValue = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.trackGripIncreasePerLap"] = x => ret.Weather.TrackGripIncreasePerLap = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.trackGripTransfer"] = x => ret.Weather.TrackGripTransfer = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.rainTimeMultiplier"] = x => ret.Weather.RainTimeMultiplier = double.Parse(x, NumberStyles.Any, CultureInfo.InvariantCulture), + ["weather.rainWetnessIncreaseTime"] = x => ret.Weather.RainWetnessIncreaseTime = TimeSpan.Parse(x, CultureInfo.InvariantCulture), + ["weather.rainWetnessDecreaseTime"] = x => ret.Weather.RainWetnessDecreaseTime = TimeSpan.Parse(x, CultureInfo.InvariantCulture), + ["weather.rainWaterIncreaseTime"] = x => ret.Weather.RainWaterIncreaseTime = TimeSpan.Parse(x, CultureInfo.InvariantCulture), + ["weather.rainWaterDecreaseTime"] = x => ret.Weather.RainWaterDecreaseTime = TimeSpan.Parse(x, CultureInfo.InvariantCulture), + }; + + foreach (var line in data + .Select(x => x.Split('#')[0].Split('=').Select(y => y.Trim()).ToArray()) + .Where(x => x.Length == 2)) { + try { + if (keys.TryGetValue(line[0], out var fn)) fn(line[1]); + } catch (Exception e) { + throw new Exception($"Failed to parse {line[1]}: {e.Message}"); + } + } + return ret; + } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0619b0c --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("AC Dynamic Conditions Server Plugin")] +[assembly: AssemblyDescription("Simple server plugin adding dynamic conditions")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("AcClub")] +[assembly: AssemblyProduct("Dynamic Conditions Server Plugin")] +[assembly: AssemblyCopyright("Copyright © AcClub, 2015-2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("3478E974-CE23-4CC7-A41B-2E868F12378F")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] \ No newline at end of file diff --git a/README.md b/README.md index a834cba..f6c2d4d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ -# plugin-dynamic-conditions - Plugin for acServer adding dynamic conditions online +# Dynamic conditions plugin for acServer + +Plugin for acServer adding dynamic conditions online. Written in C#, matches functions of live conditions in Content Manager. Based on (acplugins by Minolin)[https://github.com/minolin/acplugins]. + +# Usage: + +Prepare a config like this: + +``` +# Something to show to remember where config comes from +message=Dynamic conditions for CSP Test Server (Pursuit Mode) + +# Plugin settings +plugin.listeningPort=31634 +plugin.remotePort=31632 +plugin.remoteHostName=127.0.0.1 +plugin.serverCapacity=2 + +# Weather settings +weather.useV2=1 +weather.apiKey= +weather.trackLatitude=51.367222 +weather.trackLongitude=0.27511 +weather.trackLengthKm=1.929 +weather.trackTimezoneId=GMT Standard Time +weather.useRealConditions=0 +weather.timeOffset=00:00:00 +weather.useFixedStartingTime=0 +weather.fixedStartingTimeValue=43200 +weather.fixedStartingDateValue=05/02/2022 00:00:00 +weather.timeMultiplier=1 +weather.temperatureOffset=0 +weather.useFixedAirTemperature=0 +weather.fixedAirTemperature=25 +weather.weatherTypeChangePeriod=00:00:30 +weather.weatherTypeChangeToNeighboursOnly=0 +weather.weatherRainChance=0.05 +weather.weatherThunderChance=0.005 +weather.trackGripStartingValue=99 +weather.trackGripIncreasePerLap=0.05 +weather.trackGripTransfer=80 +``` + +CM can automatically export your settings in this form, or you can set them manually. To check the available settings, look at [ProgramParams.cs](ProgramParams.cs). + +After config is ready, simply launch the program and pass params as a command line argument. Alternatively you can append config to the executable: merge executable file with config file, add integer 0xBEE5 and an integer with the size of config file. diff --git a/Utils/DateTimeExtension.cs b/Utils/DateTimeExtension.cs new file mode 100644 index 0000000..8dccf27 --- /dev/null +++ b/Utils/DateTimeExtension.cs @@ -0,0 +1,9 @@ +using System; + +namespace AcTools.ServerPlugin.DynamicConditions.Utils { + public static class DateTimeExtension { + public static long ToUnixTimestamp(this DateTime d) { + return (long)(d.ToUniversalTime() - new DateTime(1970, 1, 1)).TotalSeconds; + } + } +} diff --git a/Utils/Half.cs b/Utils/Half.cs new file mode 100644 index 0000000..5f7b7b5 --- /dev/null +++ b/Utils/Half.cs @@ -0,0 +1,1155 @@ +using System; +using System.Diagnostics; +using System.Globalization; + +namespace SystemHalf { + /// + /// Represents a half-precision floating point number. + /// + /// + /// Note: + /// Half is not fast enought and precision is also very bad, + /// so is should not be used for mathematical computation (use Single instead). + /// The main advantage of Half type is lower memory cost: two bytes per number. + /// Half is typically used in graphical applications. + /// + /// Note: + /// All functions, where is used conversion half->float/float->half, + /// are approx. ten times slower than float->double/double->float, i.e. ~3ns on 2GHz CPU. + /// + /// References: + /// - Code retrieved from http://sourceforge.net/p/csharp-half/code/HEAD/tree/ on 2015-12-04 + /// - Fast Half Float Conversions, Jeroen van der Zijp, link: http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf + /// - IEEE 754 revision, link: http://grouper.ieee.org/groups/754/ + /// + [Serializable] + public struct Half : IComparable, IFormattable, IConvertible, IComparable, IEquatable { + /// + /// Internal representation of the half-precision floating-point number. + /// + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + internal ushort Value; + + #region Constants + /// + /// Represents the smallest positive System.Half value greater than zero. This field is constant. + /// + public static readonly Half Epsilon = ToHalf(0x0001); + + /// + /// Represents the largest possible value of System.Half. This field is constant. + /// + public static readonly Half MaxValue = ToHalf(0x7bff); + + /// + /// Represents the smallest possible value of System.Half. This field is constant. + /// + public static readonly Half MinValue = ToHalf(0xfbff); + + /// + /// Represents not a number (NaN). This field is constant. + /// + public static readonly Half NaN = ToHalf(0xfe00); + + /// + /// Represents negative infinity. This field is constant. + /// + public static readonly Half NegativeInfinity = ToHalf(0xfc00); + + /// + /// Represents positive infinity. This field is constant. + /// + public static readonly Half PositiveInfinity = ToHalf(0x7c00); + #endregion + + #region Constructors + /// + /// Initializes a new instance of System.Half to the value of the specified single-precision floating-point number. + /// + /// The value to represent as a System.Half. + public Half(float value) { + this = HalfHelper.SingleToHalf(value); + } + + /// + /// Initializes a new instance of System.Half to the value of the specified 32-bit signed integer. + /// + /// The value to represent as a System.Half. + public Half(int value) : this((float)value) { } + + /// + /// Initializes a new instance of System.Half to the value of the specified 64-bit signed integer. + /// + /// The value to represent as a System.Half. + public Half(long value) : this((float)value) { } + + /// + /// Initializes a new instance of System.Half to the value of the specified double-precision floating-point number. + /// + /// The value to represent as a System.Half. + public Half(double value) : this((float)value) { } + + /// + /// Initializes a new instance of System.Half to the value of the specified decimal number. + /// + /// The value to represent as a System.Half. + public Half(decimal value) : this((float)value) { } + + /// + /// Initializes a new instance of System.Half to the value of the specified 32-bit unsigned integer. + /// + /// The value to represent as a System.Half. + public Half(uint value) : this((float)value) { } + + /// + /// Initializes a new instance of System.Half to the value of the specified 64-bit unsigned integer. + /// + /// The value to represent as a System.Half. + public Half(ulong value) : this((float)value) { } + #endregion + + #region Numeric operators + /// + /// Returns the result of multiplying the specified System.Half value by negative one. + /// + /// A System.Half. + /// A System.Half with the value of half, but the opposite sign. -or- Zero, if half is zero. + public static Half Negate(Half half) { + return -half; + } + + /// + /// Adds two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// A System.Half value that is the sum of half1 and half2. + public static Half Add(Half half1, Half half2) { + return half1 + half2; + } + + /// + /// Subtracts one specified System.Half value from another. + /// + /// A System.Half (the minuend). + /// A System.Half (the subtrahend). + /// The System.Half result of subtracting half2 from half1. + public static Half Subtract(Half half1, Half half2) { + return half1 - half2; + } + + /// + /// Multiplies two specified System.Half values. + /// + /// A System.Half (the multiplicand). + /// A System.Half (the multiplier). + /// A System.Half that is the result of multiplying half1 and half2. + public static Half Multiply(Half half1, Half half2) { + return half1 * half2; + } + + /// + /// Divides two specified System.Half values. + /// + /// A System.Half (the dividend). + /// A System.Half (the divisor). + /// The System.Half that is the result of dividing half1 by half2. + /// half2 is zero. + public static Half Divide(Half half1, Half half2) { + return half1 / half2; + } + + /// + /// Returns the value of the System.Half operand (the sign of the operand is unchanged). + /// + /// The System.Half operand. + /// The value of the operand, half. + public static Half operator +(Half half) { + return half; + } + + /// + /// Negates the value of the specified System.Half operand. + /// + /// The System.Half operand. + /// The result of half multiplied by negative one (-1). + public static Half operator -(Half half) { + return HalfHelper.Negate(half); + } + + /// + /// Increments the System.Half operand by 1. + /// + /// The System.Half operand. + /// The value of half incremented by 1. + public static Half operator ++(Half half) { + return (Half)(half + 1f); + } + + /// + /// Decrements the System.Half operand by one. + /// + /// The System.Half operand. + /// The value of half decremented by 1. + public static Half operator --(Half half) { + return (Half)(half - 1f); + } + + /// + /// Adds two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of adding half1 and half2. + public static Half operator +(Half half1, Half half2) { + return (Half)(half1 + (float)half2); + } + + /// + /// Subtracts two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of subtracting half1 and half2. + public static Half operator -(Half half1, Half half2) { + return (Half)(half1 - (float)half2); + } + + /// + /// Multiplies two specified System.Half values. + /// + /// A System.Half. + /// A System.Half. + /// The System.Half result of multiplying half1 by half2. + public static Half operator *(Half half1, Half half2) { + return (Half)(half1 * (float)half2); + } + + /// + /// Divides two specified System.Half values. + /// + /// A System.Half (the dividend). + /// A System.Half (the divisor). + /// The System.Half result of half1 by half2. + public static Half operator /(Half half1, Half half2) { + return (Half)(half1 / (float)half2); + } + + /// + /// Returns a value indicating whether two instances of System.Half are equal. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 and half2 are equal; otherwise, false. + public static bool operator ==(Half half1, Half half2) { + return (!IsNaN(half1) && (half1.Value == half2.Value)); + } + + /// + /// Returns a value indicating whether two instances of System.Half are not equal. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 and half2 are not equal; otherwise, false. + public static bool operator !=(Half half1, Half half2) { + return half1.Value != half2.Value; + } + + /// + /// Returns a value indicating whether a specified System.Half is less than another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is less than half1; otherwise, false. + public static bool operator <(Half half1, Half half2) { + return half1 < (float)half2; + } + + /// + /// Returns a value indicating whether a specified System.Half is greater than another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is greater than half2; otherwise, false. + public static bool operator >(Half half1, Half half2) { + return half1 > (float)half2; + } + + /// + /// Returns a value indicating whether a specified System.Half is less than or equal to another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is less than or equal to half2; otherwise, false. + public static bool operator <=(Half half1, Half half2) { + return (half1 == half2) || (half1 < half2); + } + + /// + /// Returns a value indicating whether a specified System.Half is greater than or equal to another specified System.Half. + /// + /// A System.Half. + /// A System.Half. + /// true if half1 is greater than or equal to half2; otherwise, false. + public static bool operator >=(Half half1, Half half2) { + return (half1 == half2) || (half1 > half2); + } + #endregion + + #region Type casting operators + /// + /// Converts an 8-bit unsigned integer to a System.Half. + /// + /// An 8-bit unsigned integer. + /// A System.Half that represents the converted 8-bit unsigned integer. + public static implicit operator Half(byte value) { + return new Half((float)value); + } + + /// + /// Converts a 16-bit signed integer to a System.Half. + /// + /// A 16-bit signed integer. + /// A System.Half that represents the converted 16-bit signed integer. + public static implicit operator Half(short value) { + return new Half((float)value); + } + + /// + /// Converts a Unicode character to a System.Half. + /// + /// A Unicode character. + /// A System.Half that represents the converted Unicode character. + public static implicit operator Half(char value) { + return new Half((float)value); + } + + /// + /// Converts a 32-bit signed integer to a System.Half. + /// + /// A 32-bit signed integer. + /// A System.Half that represents the converted 32-bit signed integer. + public static implicit operator Half(int value) { + return new Half((float)value); + } + + /// + /// Converts a 64-bit signed integer to a System.Half. + /// + /// A 64-bit signed integer. + /// A System.Half that represents the converted 64-bit signed integer. + public static implicit operator Half(long value) { + return new Half((float)value); + } + + /// + /// Converts a single-precision floating-point number to a System.Half. + /// + /// A single-precision floating-point number. + /// A System.Half that represents the converted single-precision floating point number. + public static explicit operator Half(float value) { + return new Half(value); + } + + /// + /// Converts a double-precision floating-point number to a System.Half. + /// + /// A double-precision floating-point number. + /// A System.Half that represents the converted double-precision floating point number. + public static explicit operator Half(double value) { + return new Half((float)value); + } + + /// + /// Converts a decimal number to a System.Half. + /// + /// decimal number + /// A System.Half that represents the converted decimal number. + public static explicit operator Half(decimal value) { + return new Half((float)value); + } + + /// + /// Converts a System.Half to an 8-bit unsigned integer. + /// + /// A System.Half to convert. + /// An 8-bit unsigned integer that represents the converted System.Half. + public static explicit operator byte(Half value) { + return (byte)(float)value; + } + + /// + /// Converts a System.Half to a Unicode character. + /// + /// A System.Half to convert. + /// A Unicode character that represents the converted System.Half. + public static explicit operator char(Half value) { + return (char)(float)value; + } + + /// + /// Converts a System.Half to a 16-bit signed integer. + /// + /// A System.Half to convert. + /// A 16-bit signed integer that represents the converted System.Half. + public static explicit operator short(Half value) { + return (short)(float)value; + } + + /// + /// Converts a System.Half to a 32-bit signed integer. + /// + /// A System.Half to convert. + /// A 32-bit signed integer that represents the converted System.Half. + public static explicit operator int(Half value) { + return (int)(float)value; + } + + /// + /// Converts a System.Half to a 64-bit signed integer. + /// + /// A System.Half to convert. + /// A 64-bit signed integer that represents the converted System.Half. + public static explicit operator long(Half value) { + return (long)(float)value; + } + + /// + /// Converts a System.Half to a single-precision floating-point number. + /// + /// A System.Half to convert. + /// A single-precision floating-point number that represents the converted System.Half. + public static implicit operator float(Half value) { + return HalfHelper.HalfToSingle(value); + } + + /// + /// Converts a System.Half to a double-precision floating-point number. + /// + /// A System.Half to convert. + /// A double-precision floating-point number that represents the converted System.Half. + public static implicit operator double(Half value) { + return (float)value; + } + + /// + /// Converts a System.Half to a decimal number. + /// + /// A System.Half to convert. + /// A decimal number that represents the converted System.Half. + public static explicit operator decimal(Half value) { + return (decimal)(float)value; + } + + /// + /// Converts an 8-bit signed integer to a System.Half. + /// + /// An 8-bit signed integer. + /// A System.Half that represents the converted 8-bit signed integer. + public static implicit operator Half(sbyte value) { + return new Half((float)value); + } + + /// + /// Converts a 16-bit unsigned integer to a System.Half. + /// + /// A 16-bit unsigned integer. + /// A System.Half that represents the converted 16-bit unsigned integer. + public static implicit operator Half(ushort value) { + return new Half((float)value); + } + + /// + /// Converts a 32-bit unsigned integer to a System.Half. + /// + /// A 32-bit unsigned integer. + /// A System.Half that represents the converted 32-bit unsigned integer. + public static implicit operator Half(uint value) { + return new Half((float)value); + } + + /// + /// Converts a 64-bit unsigned integer to a System.Half. + /// + /// A 64-bit unsigned integer. + /// A System.Half that represents the converted 64-bit unsigned integer. + public static implicit operator Half(ulong value) { + return new Half((float)value); + } + + /// + /// Converts a System.Half to an 8-bit signed integer. + /// + /// A System.Half to convert. + /// An 8-bit signed integer that represents the converted System.Half. + public static explicit operator sbyte(Half value) { + return (sbyte)(float)value; + } + + /// + /// Converts a System.Half to a 16-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 16-bit unsigned integer that represents the converted System.Half. + public static explicit operator ushort(Half value) { + return (ushort)(float)value; + } + + /// + /// Converts a System.Half to a 32-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 32-bit unsigned integer that represents the converted System.Half. + public static explicit operator uint(Half value) { + return (uint)(float)value; + } + + /// + /// Converts a System.Half to a 64-bit unsigned integer. + /// + /// A System.Half to convert. + /// A 64-bit unsigned integer that represents the converted System.Half. + public static explicit operator ulong(Half value) { + return (ulong)(float)value; + } + #endregion + + /// + /// Compares this instance to a specified System.Half object. + /// + /// A System.Half object. + /// + /// A signed number indicating the relative values of this instance and value. + /// Return Value Meaning Less than zero This instance is less than value. Zero + /// This instance is equal to value. Greater than zero This instance is greater than value. + /// + public int CompareTo(Half other) { + var result = 0; + if (this < other) { + result = -1; + } else if (this > other) { + result = 1; + } else if (this != other) { + if (!IsNaN(this)) { + result = 1; + } else if (!IsNaN(other)) { + result = -1; + } + } + + return result; + } + + /// + /// Compares this instance to a specified System.Object. + /// + /// An System.Object or null. + /// + /// A signed number indicating the relative values of this instance and value. + /// Return Value Meaning Less than zero This instance is less than value. Zero + /// This instance is equal to value. Greater than zero This instance is greater + /// than value. -or- value is null. + /// + /// value is not a System.Half + public int CompareTo(object obj) { + int result; + if (obj == null) { + result = 1; + } else { + if (obj is Half half) { + result = CompareTo(half); + } else { + throw new ArgumentException("Object must be of type Half."); + } + } + + return result; + } + + /// + /// Returns a value indicating whether this instance and a specified System.Half object represent the same value. + /// + /// A System.Half object to compare to this instance. + /// true if value is equal to this instance; otherwise, false. + public bool Equals(Half other) { + return other == this || IsNaN(other) && IsNaN(this); + } + + /// + /// Returns a value indicating whether this instance and a specified System.Object + /// represent the same type and value. + /// + /// An System.Object. + /// true if value is a System.Half and equal to this instance; otherwise, false. + public override bool Equals(object obj) { + return obj is Half half && (half == this || IsNaN(half) && IsNaN(this)); + } + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() { + return Value.GetHashCode(); + } + + /// + /// Returns the System.TypeCode for value type System.Half. + /// + /// The enumerated constant (TypeCode)255. + public TypeCode GetTypeCode() { + return (TypeCode)255; + } + + #region BitConverter & Math methods for Half + /// + /// Returns the specified half-precision floating point value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 2. + public static byte[] GetBytes(Half value) { + return BitConverter.GetBytes(value.Value); + } + + /// + /// Converts the value of a specified instance of System.Half to its equivalent binary representation. + /// + /// A System.Half value. + /// A 16-bit unsigned integer that contain the binary representation of value. + public static ushort GetBits(Half value) { + return value.Value; + } + + /// + /// Returns a half-precision floating point number converted from two bytes + /// at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within value. + /// A half-precision floating point number formed by two bytes beginning at startIndex. + /// + /// startIndex is greater than or equal to the length of value minus 1, and is + /// less than or equal to the length of value minus 1. + /// + /// value is null. + /// startIndex is less than zero or greater than the length of value minus 1. + public static Half ToHalf(byte[] value, int startIndex) { + return ToHalf((ushort)BitConverter.ToInt16(value, startIndex)); + } + + /// + /// Returns a half-precision floating point number converted from its binary representation. + /// + /// Binary representation of System.Half value + /// A half-precision floating point number formed by its binary representation. + public static Half ToHalf(ushort bits) { + return new Half { Value = bits }; + } + + /// + /// Returns a value indicating the sign of a half-precision floating-point number. + /// + /// A signed number. + /// + /// A number indicating the sign of value. Number Description -1 value is less + /// than zero. 0 value is equal to zero. 1 value is greater than zero. + /// + /// value is equal to System.Half.NaN. + public static int Sign(Half value) { + if (value < 0) { + return -1; + } else if (value > 0) { + return 1; + } else { + if (value != 0) { + throw new ArithmeticException("Function does not accept floating point Not-a-Number values."); + } + } + + return 0; + } + + /// + /// Returns the absolute value of a half-precision floating-point number. + /// + /// A number in the range System.Half.MinValue ≤ value ≤ System.Half.MaxValue. + /// A half-precision floating-point number, x, such that 0 ≤ x ≤System.Half.MaxValue. + public static Half Abs(Half value) { + return HalfHelper.Abs(value); + } + + /// + /// Returns the larger of two half-precision floating-point numbers. + /// + /// The first of two half-precision floating-point numbers to compare. + /// The second of two half-precision floating-point numbers to compare. + /// + /// Parameter value1 or value2, whichever is larger. If value1, or value2, or both val1 + /// and value2 are equal to System.Half.NaN, System.Half.NaN is returned. + /// + public static Half Max(Half value1, Half value2) { + return (value1 < value2) ? value2 : value1; + } + + /// + /// Returns the smaller of two half-precision floating-point numbers. + /// + /// The first of two half-precision floating-point numbers to compare. + /// The second of two half-precision floating-point numbers to compare. + /// + /// Parameter value1 or value2, whichever is smaller. If value1, or value2, or both val1 + /// and value2 are equal to System.Half.NaN, System.Half.NaN is returned. + /// + public static Half Min(Half value1, Half value2) { + return (value1 < value2) ? value1 : value2; + } + #endregion + + /// + /// Returns a value indicating whether the specified number evaluates to not a number (System.Half.NaN). + /// + /// A half-precision floating-point number. + /// true if value evaluates to not a number (System.Half.NaN); otherwise, false. + public static bool IsNaN(Half half) { + return HalfHelper.IsNaN(half); + } + + /// + /// Returns a value indicating whether the specified number evaluates to negative or positive infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.PositiveInfinity or System.Half.NegativeInfinity; otherwise, false. + public static bool IsInfinity(Half half) { + return HalfHelper.IsInfinity(half); + } + + /// + /// Returns a value indicating whether the specified number evaluates to negative infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.NegativeInfinity; otherwise, false. + public static bool IsNegativeInfinity(Half half) { + return HalfHelper.IsNegativeInfinity(half); + } + + /// + /// Returns a value indicating whether the specified number evaluates to positive infinity. + /// + /// A half-precision floating-point number. + /// true if half evaluates to System.Half.PositiveInfinity; otherwise, false. + public static bool IsPositiveInfinity(Half half) { + return HalfHelper.IsPositiveInfinity(half); + } + + #region String operations (Parse and ToString) + /// + /// Converts the string representation of a number to its System.Half equivalent. + /// + /// The string representation of the number to convert. + /// The System.Half number equivalent to the number contained in value. + /// value is null. + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value) { + return (Half)float.Parse(value, CultureInfo.InvariantCulture); + } + + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified culture-specific format information. + /// + /// The string representation of the number to convert. + /// An System.IFormatProvider that supplies culture-specific parsing information about value. + /// The System.Half number equivalent to the number contained in s as specified by provider. + /// value is null. + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, IFormatProvider provider) { + return (Half)float.Parse(value, provider); + } + + /// + /// Converts the string representation of a number in a specified style to its System.Half equivalent. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the style elements that can be present in value. A typical value to specify is + /// System.Globalization.NumberStyles.Number. + /// + /// The System.Half number equivalent to the number contained in s as specified by style. + /// value is null. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is the + /// System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, NumberStyles style) { + return (Half)float.Parse(value, style, CultureInfo.InvariantCulture); + } + + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified style and culture-specific format. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the style elements that can be present in value. A typical value to specify is + /// System.Globalization.NumberStyles.Number. + /// + /// An System.IFormatProvider object that supplies culture-specific information about the format of value. + /// The System.Half number equivalent to the number contained in s as specified by style and provider. + /// value is null. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style is the + /// System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + /// value is not in the correct format. + /// value represents a number less than System.Half.MinValue or greater than System.Half.MaxValue. + public static Half Parse(string value, NumberStyles style, IFormatProvider provider) { + return (Half)float.Parse(value, style, provider); + } + + /// + /// Converts the string representation of a number to its System.Half equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// + /// The string representation of the number to convert. + /// + /// When this method returns, contains the System.Half number that is equivalent + /// to the numeric value contained in value, if the conversion succeeded, or is zero + /// if the conversion failed. The conversion fails if the s parameter is null, + /// is not a number in a valid format, or represents a number less than System.Half.MinValue + /// or greater than System.Half.MaxValue. This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + public static bool TryParse(string value, out Half result) { + if (float.TryParse(value, out var f)) { + result = (Half)f; + return true; + } + + result = new Half(); + return false; + } + + /// + /// Converts the string representation of a number to its System.Half equivalent + /// using the specified style and culture-specific format. A return value indicates + /// whether the conversion succeeded or failed. + /// + /// The string representation of the number to convert. + /// + /// A bitwise combination of System.Globalization.NumberStyles values that indicates + /// the permitted format of value. A typical value to specify is System.Globalization.NumberStyles.Number. + /// + /// An System.IFormatProvider object that supplies culture-specific parsing information about value. + /// + /// When this method returns, contains the System.Half number that is equivalent + /// to the numeric value contained in value, if the conversion succeeded, or is zero + /// if the conversion failed. The conversion fails if the s parameter is null, + /// is not in a format compliant with style, or represents a number less than + /// System.Half.MinValue or greater than System.Half.MaxValue. This parameter is passed uninitialized. + /// + /// true if s was converted successfully; otherwise, false. + /// + /// style is not a System.Globalization.NumberStyles value. -or- style + /// is the System.Globalization.NumberStyles.AllowHexSpecifier value. + /// + public static bool TryParse(string value, NumberStyles style, IFormatProvider provider, out Half result) { + var parseResult = false; + if (float.TryParse(value, style, provider, out var f)) { + result = (Half)f; + parseResult = true; + } else { + result = new Half(); + } + + return parseResult; + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation. + /// + /// A string that represents the value of this instance. + public override string ToString() { + return ((float)this).ToString(CultureInfo.InvariantCulture); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation + /// using the specified culture-specific format information. + /// + /// An System.IFormatProvider that supplies culture-specific formatting information. + /// The string representation of the value of this instance as specified by provider. + public string ToString(IFormatProvider formatProvider) { + return ((float)this).ToString(formatProvider); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation, using the specified format. + /// + /// A numeric format string. + /// The string representation of the value of this instance as specified by format. + public string ToString(string format) { + return ((float)this).ToString(format, CultureInfo.InvariantCulture); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation + /// using the specified format and culture-specific format information. + /// + /// A numeric format string. + /// An System.IFormatProvider that supplies culture-specific formatting information. + /// The string representation of the value of this instance as specified by format and provider. + /// format is invalid. + public string ToString(string format, IFormatProvider formatProvider) { + return ((float)this).ToString(format, formatProvider); + } + #endregion + + #region IConvertible Members + float IConvertible.ToSingle(IFormatProvider provider) { + return this; + } + + TypeCode IConvertible.GetTypeCode() { + return GetTypeCode(); + } + + bool IConvertible.ToBoolean(IFormatProvider provider) { + return Convert.ToBoolean(this); + } + + byte IConvertible.ToByte(IFormatProvider provider) { + return Convert.ToByte(this); + } + + char IConvertible.ToChar(IFormatProvider provider) { + throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "Invalid cast from '{0}' to '{1}'.", "Half", "Char")); + } + + DateTime IConvertible.ToDateTime(IFormatProvider provider) { + throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "Invalid cast from '{0}' to '{1}'.", "Half", "DateTime")); + } + + decimal IConvertible.ToDecimal(IFormatProvider provider) { + return Convert.ToDecimal(this); + } + + double IConvertible.ToDouble(IFormatProvider provider) { + return Convert.ToDouble(this); + } + + short IConvertible.ToInt16(IFormatProvider provider) { + return Convert.ToInt16(this); + } + + int IConvertible.ToInt32(IFormatProvider provider) { + return Convert.ToInt32(this); + } + + long IConvertible.ToInt64(IFormatProvider provider) { + return Convert.ToInt64(this); + } + + sbyte IConvertible.ToSByte(IFormatProvider provider) { + return Convert.ToSByte(this); + } + + string IConvertible.ToString(IFormatProvider provider) { + return Convert.ToString(this, CultureInfo.InvariantCulture); + } + + object IConvertible.ToType(Type conversionType, IFormatProvider provider) { + return (((float)this) as IConvertible).ToType(conversionType, provider); + } + + ushort IConvertible.ToUInt16(IFormatProvider provider) { + return Convert.ToUInt16(this); + } + + uint IConvertible.ToUInt32(IFormatProvider provider) { + return Convert.ToUInt32(this); + } + + ulong IConvertible.ToUInt64(IFormatProvider provider) { + return Convert.ToUInt64(this); + } + #endregion + } +} + +namespace SystemHalf { + /// + /// Helper class for Half conversions and some low level operations. + /// This class is internally used in the Half class. + /// + /// + /// References: + /// - Code retrieved from http://sourceforge.net/p/csharp-half/code/HEAD/tree/ on 2015-12-04 + /// - Fast Half Float Conversions, Jeroen van der Zijp, link: http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf + /// + internal static class HalfHelper { + private static readonly uint[] MantissaTable = GenerateMantissaTable(); + private static readonly uint[] ExponentTable = GenerateExponentTable(); + private static readonly ushort[] OffsetTable = GenerateOffsetTable(); + private static readonly ushort[] BaseTable = GenerateBaseTable(); + private static readonly sbyte[] ShiftTable = GenerateShiftTable(); + + // Transforms the subnormal representation to a normalized one. + private static uint ConvertMantissa(int i) { + var m = (uint)(i << 13); // Zero pad mantissa bits + uint e = 0; // Zero exponent + + // While not normalized + while ((m & 0x00800000) == 0) { + e -= 0x00800000; // Decrement exponent (1<<23) + m <<= 1; // Shift mantissa + } + m &= unchecked((uint)~0x00800000); // Clear leading 1 bit + e += 0x38800000; // Adjust bias ((127-14)<<23) + return m | e; // Return combined number + } + + private static uint[] GenerateMantissaTable() { + var mantissaTable = new uint[2048]; + mantissaTable[0] = 0; + for (var i = 1; i < 1024; i++) { + mantissaTable[i] = ConvertMantissa(i); + } + for (var i = 1024; i < 2048; i++) { + mantissaTable[i] = (uint)(0x38000000 + ((i - 1024) << 13)); + } + + return mantissaTable; + } + + private static uint[] GenerateExponentTable() { + var exponentTable = new uint[64]; + exponentTable[0] = 0; + for (var i = 1; i < 31; i++) { + exponentTable[i] = (uint)(i << 23); + } + exponentTable[31] = 0x47800000; + exponentTable[32] = 0x80000000; + for (var i = 33; i < 63; i++) { + exponentTable[i] = (uint)(0x80000000 + ((i - 32) << 23)); + } + exponentTable[63] = 0xc7800000; + + return exponentTable; + } + + private static ushort[] GenerateOffsetTable() { + var offsetTable = new ushort[64]; + offsetTable[0] = 0; + for (var i = 1; i < 32; i++) { + offsetTable[i] = 1024; + } + offsetTable[32] = 0; + for (var i = 33; i < 64; i++) { + offsetTable[i] = 1024; + } + + return offsetTable; + } + + private static ushort[] GenerateBaseTable() { + var baseTable = new ushort[512]; + for (var i = 0; i < 256; ++i) { + var e = (sbyte)(127 - i); + if (e > 24) { + // Very small numbers map to zero + baseTable[i | 0x000] = 0x0000; + baseTable[i | 0x100] = 0x8000; + } else if (e > 14) { + // Small numbers map to denorms + baseTable[i | 0x000] = (ushort)(0x0400 >> (18 + e)); + baseTable[i | 0x100] = (ushort)((0x0400 >> (18 + e)) | 0x8000); + } else if (e >= -15) { + // Normal numbers just lose precision + baseTable[i | 0x000] = (ushort)((15 - e) << 10); + baseTable[i | 0x100] = (ushort)(((15 - e) << 10) | 0x8000); + } else if (e > -128) { + // Large numbers map to Infinity + baseTable[i | 0x000] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + } else { + // Infinity and NaN's stay Infinity and NaN's + baseTable[i | 0x000] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + } + } + + return baseTable; + } + + private static sbyte[] GenerateShiftTable() { + var shiftTable = new sbyte[512]; + for (var i = 0; i < 256; ++i) { + var e = (sbyte)(127 - i); + if (e > 24) { + // Very small numbers map to zero + shiftTable[i | 0x000] = 24; + shiftTable[i | 0x100] = 24; + } else if (e > 14) { + // Small numbers map to denorms + shiftTable[i | 0x000] = (sbyte)(e - 1); + shiftTable[i | 0x100] = (sbyte)(e - 1); + } else if (e >= -15) { + // Normal numbers just lose precision + shiftTable[i | 0x000] = 13; + shiftTable[i | 0x100] = 13; + } else if (e > -128) { + // Large numbers map to Infinity + shiftTable[i | 0x000] = 24; + shiftTable[i | 0x100] = 24; + } else { + // Infinity and NaN's stay Infinity and NaN's + shiftTable[i | 0x000] = 13; + shiftTable[i | 0x100] = 13; + } + } + + return shiftTable; + } + + public static unsafe float HalfToSingle(Half half) { + var result = MantissaTable[OffsetTable[half.Value >> 10] + (half.Value & 0x3ff)] + ExponentTable[half.Value >> 10]; + return *(float*)&result; + } + + public static unsafe Half SingleToHalf(float single) { + var value = *(uint*)&single; + var result = (ushort)(BaseTable[(value >> 23) & 0x1ff] + ((value & 0x007fffff) >> ShiftTable[value >> 23])); + return Half.ToHalf(result); + } + + public static Half Negate(Half half) { + return Half.ToHalf((ushort)(half.Value ^ 0x8000)); + } + + public static Half Abs(Half half) { + return Half.ToHalf((ushort)(half.Value & 0x7fff)); + } + + public static bool IsNaN(Half half) { + return (half.Value & 0x7fff) > 0x7c00; + } + + public static bool IsInfinity(Half half) { + return (half.Value & 0x7fff) == 0x7c00; + } + + public static bool IsPositiveInfinity(Half half) { + return half.Value == 0x7c00; + } + + public static bool IsNegativeInfinity(Half half) { + return half.Value == 0xfc00; + } + } +} \ No newline at end of file diff --git a/Utils/Logging.cs b/Utils/Logging.cs new file mode 100644 index 0000000..19c7d0b --- /dev/null +++ b/Utils/Logging.cs @@ -0,0 +1,20 @@ +using System; + +namespace AcTools.ServerPlugin.DynamicConditions.Utils { + public class Logging { + public static void Write(object msg) { + Console.Write(@"> "); + Console.WriteLine(msg); + } + + public static void Warning(object msg) { + Console.Write(@"! "); + Console.WriteLine(msg); + } + + public static void Debug(object msg) { + Console.Write(@". "); + Console.WriteLine(msg); + } + } +} \ No newline at end of file diff --git a/Utils/MainExecutingFile.cs b/Utils/MainExecutingFile.cs new file mode 100644 index 0000000..052dfba --- /dev/null +++ b/Utils/MainExecutingFile.cs @@ -0,0 +1,13 @@ +using System.IO; +using System.Reflection; + +namespace AcTools.ServerPlugin.DynamicConditions.Utils { + internal static class MainExecutingFile { + private static string _location; + private static string _directory; + + public static string Location => _location ?? (_location = Assembly.GetEntryAssembly()?.Location ?? ""); + + public static string Directory => _directory ?? (_directory = Path.GetDirectoryName(Location)); + } +} diff --git a/Utils/MathUtils.cs b/Utils/MathUtils.cs new file mode 100644 index 0000000..f1bf78c --- /dev/null +++ b/Utils/MathUtils.cs @@ -0,0 +1,111 @@ +using System; +using JetBrains.Annotations; + +namespace AcTools.ServerPlugin.DynamicConditions.Utils { + // TODO: Remove MathF + public static class MathUtils { + public static double Pow(this double v, double p) => Math.Pow(v, p); + public static float Pow(this float v, float p) => (float)Math.Pow(v, p); + + public static double Sqrt(this double v) => Math.Sqrt(v); + public static float Sqrt(this float v) => (float)Math.Sqrt(v); + + public static double Acos(this double v) => Math.Acos(v); + public static float Acos(this float v) => (float)Math.Acos(v); + + public static double Asin(this double v) => Math.Asin(v); + public static float Asin(this float v) => (float)Math.Asin(v); + + public static double Sin(this double v) => Math.Sin(v); + public static float Sin(this float v) => (float)Math.Sin(v); + + public static double Cos(this double v) => Math.Cos(v); + public static float Cos(this float v) => (float)Math.Cos(v); + + public static double Tan(this double a) => Math.Tan(a); + public static float Tan(this float a) => (float)Math.Tan(a); + + public static double Atan(this double f) => Math.Atan(f); + public static float Atan(this float f) => (float)Math.Atan(f); + + public static int Abs(this int v) => v < 0 ? -v : v; + public static double Abs(this double v) => v < 0d ? -v : v; + public static float Abs(this float v) => v < 0f ? -v : v; + + public static bool IsFinite(this double v) => !double.IsInfinity(v) && !double.IsNaN(v); + public static bool IsFinite(this float v) => !float.IsInfinity(v) && !float.IsNaN(v); + + public static int Clamp(this int v, int min, int max) => v < min ? min : v > max ? max : v; + public static float Clamp(this float v, float min, float max) => v < min ? min : v > max ? max : v; + public static double Clamp(this double v, double min, double max) => v < min ? min : v > max ? max : v; + public static TimeSpan Clamp(this TimeSpan v, TimeSpan min, TimeSpan max) => v < min ? min : v > max ? max : v; + + public static byte ClampToByte(this double v) => (byte)(v < 0d ? 0 : v > 255d ? 255 : v); + public static byte ClampToByte(this int v) => (byte)(v < 0 ? 0 : v > 255 ? 255 : v); + + public static float Saturate(this float value) => value < 0f ? 0f : value > 1f ? 1f : value; + public static double Saturate(this double value) => value < 0d ? 0d : value > 1d ? 1d : value; + + public static int Sign(this int value) => value < 0 ? -1 : value > 0 ? 1 : 0; + public static int Sign(this float value) => value < 0f ? -1 : value > 0f ? 1 : 0; + public static int Sign(this double value) => value < 0d ? -1 : value > 0d ? 1 : 0; + + public static int RoundToInt(this double value) => (int)Math.Round(value); + + [ThreadStatic] + private static Random _random; + + [NotNull] + public static Random RandomInstance => _random ?? (_random = new Random(Guid.NewGuid().GetHashCode())); + + public static int Random(int maxValueExclusive) => RandomInstance.Next(maxValueExclusive); + public static int Random(int minValueInclusive, int maxValueExclusive) => RandomInstance.Next(minValueInclusive, maxValueExclusive); + public static double Random() => RandomInstance.NextDouble(); + public static double Random(double maxValue) => RandomInstance.NextDouble() * maxValue; + public static double Random(double minValue, double maxValue) => Random(maxValue - minValue) + minValue; + public static float Random(float maxValue) => (float)(RandomInstance.NextDouble() * maxValue); + public static float Random(float minValue, float maxValue) => Random(maxValue - minValue) + minValue; + public static TimeSpan Random(TimeSpan minValue, TimeSpan maxValue) => TimeSpan.FromSeconds(Random(minValue.TotalSeconds, maxValue.TotalSeconds)); + + public static TimeSpan Max(this TimeSpan a, TimeSpan b) => a > b ? a : b; + public static TimeSpan Min(this TimeSpan a, TimeSpan b) => a < b ? a : b; + + public static float Lerp(this float t, float v0, float v1) => (1f - t) * v0 + t * v1; + public static double Lerp(this double t, double v0, double v1) => (1d - t) * v0 + t * v1; + + public static float LerpInv(this float t, float v0, float v1) => (t - v0) / (v1 - v0); + public static double LerpInv(this double t, double v0, double v1) => (t - v0) / (v1 - v0); + + public static float LerpInvSat(this float t, float v0, float v1) => t.LerpInv(v0, v1).Saturate(); + public static double LerpInvSat(this double t, double v0, double v1) => t.LerpInv(v0, v1).Saturate(); + + public static float Remap(this float t, float v0, float v1, float o0, float o1) => (t - v0) / (v1 - v0) * (o1 - o0) + o0; + public static double Remap(this double t, double v0, double v1, double o0, double o1) => (t - v0) / (v1 - v0) * (o1 - o0) + o0; + + /// + /// For normalized and saturated X. + /// + public static float SmootherStep(this float x) => x * x * x * (x * (x * 6f - 15f) + 10f); + + /// + /// For normalized and saturated X. + /// + public static double SmootherStep(this double x) => x * x * x * (x * (x * 6d - 15d) + 10d); + + public static float SmootherStep(this float x, float edge0, float edge1) => ((x - edge0) / (edge1 - edge0)).Saturate().SmootherStep(); + public static double SmootherStep(this double x, double edge0, double edge1) => ((x - edge0) / (edge1 - edge0)).Saturate().SmootherStep(); + + /// + /// For normalized and saturated X. + /// + public static float SmoothStep(this float x) => x * x * (3f - 2f * x); + + /// + /// For normalized and saturated X. + /// + public static double SmoothStep(this double x) => x * x * (3d - 2d * x); + + public static float SmoothStep(this float x, float edge0, float edge1) => ((x - edge0) / (edge1 - edge0)).Saturate().SmoothStep(); + public static double SmoothStep(this double x, double edge0, double edge1) => ((x - edge0) / (edge1 - edge0)).Saturate().SmoothStep(); + } +} \ No newline at end of file diff --git a/Utils/TaskExtension.cs b/Utils/TaskExtension.cs new file mode 100644 index 0000000..521b4bd --- /dev/null +++ b/Utils/TaskExtension.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace AcTools.ServerPlugin.DynamicConditions.Utils { + public static class TaskExtension { + public static void Ignore(this Task task) { + task.ContinueWith(x => { + Logging.Write(x.Exception?.Flatten()); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + } +} \ No newline at end of file diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..661b75c --- /dev/null +++ b/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file