diff --git a/AssettoServer.sln b/AssettoServer.sln
index 56113bd2..8de0e2b6 100644
--- a/AssettoServer.sln
+++ b/AssettoServer.sln
@@ -43,6 +43,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagModePlugin", "TagModePlu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastTravelPlugin", "FastTravelPlugin\FastTravelPlugin.csproj", "{022C75A2-8B36-44F5-B99C-AF577A0192FD}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrafficAiPlugin", "TrafficAiPlugin\TrafficAiPlugin.csproj", "{F20A785C-686A-40A4-8821-28627BA6FA48}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrafficAiPlugin.Shared", "TrafficAiPlugin.Shared\TrafficAiPlugin.Shared.csproj", "{18EE29A8-AA8F-47AD-849B-AD5D04C5A069}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -129,6 +133,14 @@ Global
{022C75A2-8B36-44F5-B99C-AF577A0192FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{022C75A2-8B36-44F5-B99C-AF577A0192FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{022C75A2-8B36-44F5-B99C-AF577A0192FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F20A785C-686A-40A4-8821-28627BA6FA48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F20A785C-686A-40A4-8821-28627BA6FA48}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F20A785C-686A-40A4-8821-28627BA6FA48}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F20A785C-686A-40A4-8821-28627BA6FA48}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18EE29A8-AA8F-47AD-849B-AD5D04C5A069}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {18EE29A8-AA8F-47AD-849B-AD5D04C5A069}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18EE29A8-AA8F-47AD-849B-AD5D04C5A069}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {18EE29A8-AA8F-47AD-849B-AD5D04C5A069}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/AssettoServer/AssettoServer.csproj b/AssettoServer/AssettoServer.csproj
index 2c853866..c02692c5 100644
--- a/AssettoServer/AssettoServer.csproj
+++ b/AssettoServer/AssettoServer.csproj
@@ -138,8 +138,6 @@
-
-
diff --git a/AssettoServer/Commands/Modules/AiTrafficModule.cs b/AssettoServer/Commands/Modules/AiTrafficModule.cs
deleted file mode 100644
index dbf38996..00000000
--- a/AssettoServer/Commands/Modules/AiTrafficModule.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Linq;
-using AssettoServer.Commands.Attributes;
-using AssettoServer.Server;
-using AssettoServer.Server.Configuration;
-using JetBrains.Annotations;
-using Qmmands;
-
-namespace AssettoServer.Commands.Modules;
-
-[RequireAdmin]
-[UsedImplicitly(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.WithMembers)]
-public class AiTrafficModule : ACModuleBase
-{
- private readonly ACServerConfiguration _configuration;
- private readonly EntryCarManager _entryCarManager;
-
- public AiTrafficModule(ACServerConfiguration configuration, EntryCarManager entryCarManager)
- {
- _configuration = configuration;
- _entryCarManager = entryCarManager;
- }
-
- [Command("setaioverbooking")]
- public void SetAiOverbooking(int count)
- {
- if (!_configuration.Extra.EnableAi)
- {
- Reply("AI disabled");
- return;
- }
-
- foreach (var aiCar in _entryCarManager.EntryCars.Where(car => car.AiControlled && car.Client == null))
- {
- aiCar.SetAiOverbooking(count);
- }
- Reply($"AI overbooking set to {count}");
- }
-}
diff --git a/AssettoServer/Commands/Modules/GeneralModule.cs b/AssettoServer/Commands/Modules/GeneralModule.cs
index d2991786..8967619a 100644
--- a/AssettoServer/Commands/Modules/GeneralModule.cs
+++ b/AssettoServer/Commands/Modules/GeneralModule.cs
@@ -67,17 +67,4 @@ public async Task ShowLegalNotice()
Reply(line);
}
}
-
- [Command("resetcar"), RequireConnectedPlayer]
- public void ResetCarAsync()
- {
- if (_configuration.Extra is { EnableClientMessages: true, EnableCarReset: true, MinimumCSPVersion: >= CSPVersion.V0_2_8, EnableAi: true })
- {
- Reply(Client!.EntryCar.TryResetPosition()
- ? "Position successfully reset"
- : "Couldn't reset position");
- }
- else
- Reply("Reset is not enabled on this server");
- }
}
diff --git a/AssettoServer/Network/CSPClientMessageHandler.cs b/AssettoServer/Network/CSPClientMessageHandler.cs
index bd60926a..002b2461 100644
--- a/AssettoServer/Network/CSPClientMessageHandler.cs
+++ b/AssettoServer/Network/CSPClientMessageHandler.cs
@@ -26,7 +26,6 @@ public CSPClientMessageHandler(CSPClientMessageTypeManager cspClientMessageTypeM
cspClientMessageTypeManager.RegisterOnlineEvent((_, _) => { });
cspClientMessageTypeManager.RegisterOnlineEvent((_, _) => { });
- cspClientMessageTypeManager.RegisterOnlineEvent(OnResetCar);
cspClientMessageTypeManager.RegisterOnlineEvent(OnLuaReady);
}
@@ -201,12 +200,6 @@ private void OnAdminPenaltyOut(ACTcpClient sender, PacketReader reader)
}
}
- private void OnResetCar(ACTcpClient sender, RequestResetPacket packet)
- {
- if (!_configuration.Extra.EnableCarReset) return;
- sender.EntryCar.TryResetPosition();
- }
-
private void OnLuaReady(ACTcpClient sender, LuaReadyPacket packet)
{
sender.FireLuaReady();
diff --git a/AssettoServer/Network/Tcp/ACTcpClient.cs b/AssettoServer/Network/Tcp/ACTcpClient.cs
index 48a457e2..53e80189 100644
--- a/AssettoServer/Network/Tcp/ACTcpClient.cs
+++ b/AssettoServer/Network/Tcp/ACTcpClient.cs
@@ -433,9 +433,6 @@ private async Task ReceiveLoopAsync()
CSPVersion = cspVersion;
}
- // Gracefully despawn AI cars
- EntryCar.SetAiOverbooking(0);
-
if (_configuration.Server.CheckAdminPassword(handshakeRequest.Password))
IsAdministrator = true;
@@ -872,15 +869,6 @@ private async Task SendFirstUpdateAsync()
batched.Packets.Add(new P2PUpdate { SessionId = car.SessionId, P2PCount = car.Status.P2PCount });
batched.Packets.Add(new BallastUpdate { SessionId = car.SessionId, BallastKg = car.Ballast, Restrictor = car.Restrictor });
-
- if (_configuration.Extra.AiParams.HideAiCars)
- {
- batched.Packets.Add(new CSPCarVisibilityUpdate
- {
- SessionId = car.SessionId,
- Visible = car.AiControlled ? CSPCarVisibility.Invisible : CSPCarVisibility.Visible
- });
- }
}
if (EntryCar.FixedSetup != null
diff --git a/AssettoServer/Server/ACServer.cs b/AssettoServer/Server/ACServer.cs
index 36a1d8ed..5d20771c 100644
--- a/AssettoServer/Server/ACServer.cs
+++ b/AssettoServer/Server/ACServer.cs
@@ -5,7 +5,7 @@
using System.Collections.Generic;
using System.Reflection;
using AssettoServer.Server.Configuration;
-using AssettoServer.Server.Ai.Splines;
+using AssettoServer.Network.Udp;
using AssettoServer.Server.Blacklist;
using AssettoServer.Server.GeoParams;
using AssettoServer.Server.Whitelist;
@@ -42,8 +42,7 @@ public ACServer(
ChecksumManager checksumManager,
CSPFeatureManager cspFeatureManager,
CSPServerScriptProvider cspServerScriptProvider,
- IHostApplicationLifetime applicationLifetime,
- AiSpline? aiSpline = null)
+ IHostApplicationLifetime applicationLifetime)
{
Log.Information("Starting server");
@@ -86,17 +85,8 @@ public ACServer(
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("AssettoServer.Server.Lua.assettoserver.lua")!;
cspServerScriptProvider.AddScript(stream, "assettoserver.lua");
-
- if (_configuration.Extra.EnableCarReset)
- {
- if (!_configuration.Extra.EnableClientMessages || _configuration.CSPTrackOptions.MinimumCSPVersion < CSPVersion.V0_2_8 || aiSpline == null)
- {
- throw new ConfigurationException(
- "Reset car: Minimum required CSP version of 0.2.8 (3424); Requires enabled client messages; Requires working AI spline");
- }
- }
}
-
+
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
Log.Information("Starting HTTP server on port {HttpPort}", _configuration.Server.HttpPort);
@@ -183,26 +173,25 @@ private void MainLoop(CancellationToken stoppingToken)
}
}
- if (fromCar.AiControlled || fromCar.HasUpdateToSend)
+ if (!fromCar.HasUpdateToSend) continue;
+
+ fromCar.HasUpdateToSend = false;
+
+ for (int j = 0; j < _entryCarManager.EntryCars.Length; j++)
{
- fromCar.HasUpdateToSend = false;
+ var toCar = _entryCarManager.EntryCars[j];
+ var toClient = toCar.Client;
+ if (toCar == fromCar
+ || toClient == null || !toClient.HasSentFirstUpdate || !toClient.HasUdpEndpoint
+ || !fromCar.GetPositionUpdateForCar(toCar, out var update)) continue;
- for (int j = 0; j < _entryCarManager.EntryCars.Length; j++)
+ if (toClient.SupportsCSPCustomUpdate)
{
- var toCar = _entryCarManager.EntryCars[j];
- var toClient = toCar.Client;
- if (toCar == fromCar
- || toClient == null || !toClient.HasSentFirstUpdate || toClient.UdpEndpoint == null
- || !fromCar.GetPositionUpdateForCar(toCar, out var update)) continue;
-
- if (toClient.SupportsCSPCustomUpdate || fromCar.AiControlled)
- {
- positionUpdates[toCar].Add(update);
- }
- else
- {
- toClient.SendPacketUdp(in update);
- }
+ positionUpdates[toCar].Add(update);
+ }
+ else
+ {
+ toClient.SendPacketUdp(in update);
}
}
}
diff --git a/AssettoServer/Server/Ai/AiModule.cs b/AssettoServer/Server/Ai/AiModule.cs
index 65666f71..5f282702 100644
--- a/AssettoServer/Server/Ai/AiModule.cs
+++ b/AssettoServer/Server/Ai/AiModule.cs
@@ -1,39 +1 @@
-using AssettoServer.Server.Ai.Splines;
-using AssettoServer.Server.Configuration;
-using AssettoServer.Server.OpenSlotFilters;
-using Autofac;
-using Microsoft.Extensions.Hosting;
-
-namespace AssettoServer.Server.Ai;
-
-public class AiModule : Module
-{
- private readonly ACServerConfiguration _configuration;
-
- public AiModule(ACServerConfiguration configuration)
- {
- _configuration = configuration;
- }
-
- protected override void Load(ContainerBuilder builder)
- {
- builder.RegisterType().AsSelf();
-
- if (_configuration.Extra.EnableAi)
- {
- builder.RegisterType().AsSelf().As().SingleInstance();
- builder.RegisterType().AsSelf().SingleInstance().AutoActivate();
- builder.RegisterType().As();
-
- if (_configuration.Extra.AiParams.HourlyTrafficDensity != null)
- {
- builder.RegisterType().As().SingleInstance();
- }
-
- builder.RegisterType().AsSelf();
- builder.RegisterType().AsSelf();
- builder.RegisterType().AsSelf();
- builder.Register((AiSplineLocator locator) => locator.Locate()).AsSelf().SingleInstance();
- }
- }
-}
+
\ No newline at end of file
diff --git a/AssettoServer/Server/Ai/AiUpdater.cs b/AssettoServer/Server/Ai/AiUpdater.cs
deleted file mode 100644
index db3ad139..00000000
--- a/AssettoServer/Server/Ai/AiUpdater.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-
-namespace AssettoServer.Server.Ai;
-
-public class AiUpdater
-{
- private readonly EntryCarManager _entryCarManager;
-
- public AiUpdater(EntryCarManager entryCarManager, ACServer server)
- {
- _entryCarManager = entryCarManager;
- server.Update += OnUpdate;
- }
-
- private void OnUpdate(object sender, EventArgs args)
- {
- for (var i = 0; i < _entryCarManager.EntryCars.Length; i++)
- {
- var entryCar = _entryCarManager.EntryCars[i];
- if (entryCar.AiControlled)
- {
- entryCar.AiUpdate();
- }
- }
- }
-}
diff --git a/AssettoServer/Server/Configuration/ACServerConfiguration.cs b/AssettoServer/Server/Configuration/ACServerConfiguration.cs
index 1811e116..a21ccddf 100644
--- a/AssettoServer/Server/Configuration/ACServerConfiguration.cs
+++ b/AssettoServer/Server/Configuration/ACServerConfiguration.cs
@@ -276,27 +276,6 @@ private void ApplyConfigurationFixes()
Server.MaxClients = EntryList.Cars.Count;
}
- if (Extra is { EnableAi: true, AiParams.AutoAssignTrafficCars: true })
- {
- foreach (var entry in EntryList.Cars)
- {
- if (entry.Model.Contains("traffic"))
- {
- entry.AiMode = AiMode.Fixed;
- }
- }
- }
-
- if (Extra.AiParams.AiPerPlayerTargetCount == 0)
- {
- Extra.AiParams.AiPerPlayerTargetCount = EntryList.Cars.Count(c => c.AiMode != AiMode.None);
- }
-
- if (Extra.AiParams.MaxAiTargetCount == 0)
- {
- Extra.AiParams.MaxAiTargetCount = EntryList.Cars.Count(c => c.AiMode != AiMode.Fixed) * Extra.AiParams.AiPerPlayerTargetCount;
- }
-
var filteredServerName = ServerDetailsIdRegex().Replace(Server.Name, "");
if (filteredServerName != Server.Name)
{
diff --git a/AssettoServer/Server/Configuration/ACServerConfigurationValidator.cs b/AssettoServer/Server/Configuration/ACServerConfigurationValidator.cs
index 968af5dd..f276a170 100644
--- a/AssettoServer/Server/Configuration/ACServerConfigurationValidator.cs
+++ b/AssettoServer/Server/Configuration/ACServerConfigurationValidator.cs
@@ -21,31 +21,6 @@ public ACServerConfigurationValidator()
extra.RuleFor(x => x.WhitelistUserGroup).NotEmpty();
extra.RuleFor(x => x.AdminUserGroup).NotEmpty();
extra.RuleFor(x => x.VoteKickMinimumConnectedPlayers).GreaterThanOrEqualTo((ushort)3);
-
- extra.RuleFor(x => x.AiParams).ChildRules(aiParams =>
- {
- aiParams.RuleFor(ai => ai.MinSpawnDistancePoints).LessThanOrEqualTo(ai => ai.MaxSpawnDistancePoints);
- aiParams.RuleFor(ai => ai.MinAiSafetyDistanceMeters).LessThanOrEqualTo(ai => ai.MaxAiSafetyDistanceMeters);
- aiParams.RuleFor(ai => ai.MinSpawnProtectionTimeSeconds).LessThanOrEqualTo(ai => ai.MaxSpawnProtectionTimeSeconds);
- aiParams.RuleFor(ai => ai.MinCollisionStopTimeSeconds).LessThanOrEqualTo(ai => ai.MaxCollisionStopTimeSeconds);
- aiParams.RuleFor(ai => ai.MaxSpeedVariationPercent).InclusiveBetween(0, 1);
- aiParams.RuleFor(ai => ai.DefaultAcceleration).GreaterThan(0);
- aiParams.RuleFor(ai => ai.DefaultDeceleration).GreaterThan(0);
- aiParams.RuleFor(ai => ai.NamePrefix).NotNull();
- aiParams.RuleFor(ai => ai.IgnoreObstaclesAfterSeconds).GreaterThanOrEqualTo(0);
- aiParams.RuleFor(ai => ai.HourlyTrafficDensity)
- .Must(htd => htd?.Count == 24)
- .When(ai => ai.HourlyTrafficDensity != null)
- .WithMessage("HourlyTrafficDensity must have exactly 24 entries");
- aiParams.RuleFor(ai => ai.CarSpecificOverrides).NotNull();
- aiParams.RuleFor(ai => ai.AiBehaviorUpdateIntervalHz).GreaterThan(0);
- aiParams.RuleFor(ai => ai.LaneCountSpecificOverrides).NotNull();
- aiParams.RuleForEach(ai => ai.LaneCountSpecificOverrides).ChildRules(overrides =>
- {
- overrides.RuleFor(o => o.Key).GreaterThan(0);
- overrides.RuleFor(o => o.Value.MinAiSafetyDistanceMeters).LessThanOrEqualTo(o => o.Value.MaxAiSafetyDistanceMeters);
- });
- });
});
RuleFor(cfg => cfg.Server).ChildRules(server =>
diff --git a/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs b/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs
index de7beaef..01f7b802 100644
--- a/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs
+++ b/AssettoServer/Server/Configuration/Extra/ACExtraConfiguration.cs
@@ -29,8 +29,6 @@ public partial class ACExtraConfiguration : ObservableObject
public int MandatoryClientSecurityLevel { get; internal set; }
[YamlMember(Description = "Force headlights on for all cars")]
public bool ForceLights { get; set; }
- [YamlMember(Description = "Enable usage of /resetcar to teleport the player to the closest spline point. Requires CSP v0.2.8 (3424) or later")]
- public bool EnableCarReset { get; set; } = false;
[YamlMember(Description = "Enable vanilla server voting for: Session skip; Session restart")]
public bool EnableSessionVote { get; set; } = true;
[YamlMember(Description = "Enable vanilla server voting to kick a player")]
@@ -57,8 +55,6 @@ public partial class ACExtraConfiguration : ObservableObject
public bool LockServerDate { get; set; } = true;
[YamlMember(Description = "Reduce track grip when the track is wet. This is much worse than proper CSP rain physics but allows you to run clients with public/Patreon CSP at the same time")]
public double RainTrackGripReductionPercent { get; set; } = 0;
- [YamlMember(Description = "Enable AI traffic")]
- public bool EnableAi { get; init; } = false;
[YamlMember(Description = "Override the country shown in CM. Please do not use this unless the autodetected country is wrong", DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
public List? GeoParamsCountryOverride { get; init; } = null;
[YamlMember(Description = "List of plugins to enable")]
@@ -115,8 +111,6 @@ public partial class ACExtraConfiguration : ObservableObject
[YamlMember(Description = "Allow a user group to execute specific admin commands")]
public List? UserGroupCommandPermissions { get; init; }
- public AiParams AiParams { get; init; } = new();
-
[YamlIgnore] internal bool ContainsObsoletePluginConfiguration { get; private set; }
public void ToFile(string path)
@@ -170,61 +164,5 @@ public static ACExtraConfiguration FromFile(string path)
]
}
],
- AiParams = new AiParams
- {
- CarSpecificOverrides = [
- new CarSpecificOverrides
- {
- Model = "my_car_model",
- Acceleration = 2.5f,
- Deceleration = 8.5f,
- AllowedLanes = [LaneSpawnBehavior.Left, LaneSpawnBehavior.Middle, LaneSpawnBehavior.Right],
- MaxOverbooking = 1,
- CorneringSpeedFactor = 0.5f,
- CorneringBrakeDistanceFactor = 3,
- CorneringBrakeForceFactor = 0.5f,
- EngineIdleRpm = 800,
- EngineMaxRpm = 3000,
- MaxLaneCount = 2,
- MinLaneCount = 1,
- TyreDiameterMeters = 0.8f,
- SplineHeightOffsetMeters = 0,
- VehicleLengthPostMeters = 2,
- VehicleLengthPreMeters = 2,
- MinAiSafetyDistanceMeters = 20,
- MaxAiSafetyDistanceMeters = 25,
- MinCollisionStopTimeSeconds = 0,
- MaxCollisionStopTimeSeconds = 0,
- MinSpawnProtectionTimeSeconds = 30,
- MaxSpawnProtectionTimeSeconds = 60
- }
- ],
- LaneCountSpecificOverrides = new Dictionary
- {
- {
- 1,
- new LaneCountSpecificOverrides
- {
- MinAiSafetyDistanceMeters = 50,
- MaxAiSafetyDistanceMeters = 100
- }
- },
- {
- 2,
- new LaneCountSpecificOverrides
- {
- MinAiSafetyDistanceMeters = 40,
- MaxAiSafetyDistanceMeters = 80
- }
- }
- },
- IgnorePlayerObstacleSpheres = [
- new Sphere
- {
- Center = new Vector3(0, 0, 0),
- RadiusMeters = 50
- }
- ]
- }
};
}
diff --git a/AssettoServer/Server/Configuration/Kunos/EntryList.cs b/AssettoServer/Server/Configuration/Kunos/EntryList.cs
index 74bdaf3a..aa06d153 100644
--- a/AssettoServer/Server/Configuration/Kunos/EntryList.cs
+++ b/AssettoServer/Server/Configuration/Kunos/EntryList.cs
@@ -24,7 +24,7 @@ public class Entry
[IniField("TEAM")] public string? Team { get; init; }
[IniField("FIXED_SETUP")] public string? FixedSetup { get; init; } = null;
[IniField("GUID")] public string Guid { get; init; } = "";
- [IniField("AI")] public AiMode AiMode { get; internal set; } = AiMode.None;
+ [IniField("AI")] public AiMode AiMode { get; set; } = AiMode.None;
[IniField("LEGAL_TYRES")] public string? LegalTyres { get; init; }
}
diff --git a/AssettoServer/Server/EntryCar.cs b/AssettoServer/Server/EntryCar.cs
index 642d01ce..e37a5e4c 100644
--- a/AssettoServer/Server/EntryCar.cs
+++ b/AssettoServer/Server/EntryCar.cs
@@ -1,22 +1,25 @@
-using AssettoServer.Network.Tcp;
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
-using System.Threading.Tasks;
using AssettoServer.Network.ClientMessages;
-using AssettoServer.Server.Ai;
-using AssettoServer.Server.Ai.Splines;
+using AssettoServer.Network.Tcp;
using AssettoServer.Server.Configuration;
using AssettoServer.Shared.Model;
using AssettoServer.Shared.Network.Packets.Incoming;
using AssettoServer.Shared.Network.Packets.Outgoing;
-using AssettoServer.Shared.Network.Packets.Shared;
using Serilog;
using Serilog.Core;
using Serilog.Events;
namespace AssettoServer.Server;
+public enum AiMode
+{
+ None,
+ Auto,
+ Fixed
+}
+
public partial class EntryCar : IEntryCar
{
public ACTcpClient? Client { get; internal set; }
@@ -48,10 +51,14 @@ public partial class EntryCar : IEntryCar
public float NetworkDistanceSquared { get; internal set; }
public int OutsideNetworkBubbleUpdateRateMs { get; internal set; }
- internal long[] OtherCarsLastSentUpdateTime { get; }
+ public long[] OtherCarsLastSentUpdateTime { get; }
public EntryCar? TargetCar { get; set; }
private long LastFallCheckTime{ get; set; }
+ public bool AiControlled { get; set; } = false;
+ public AiMode AiMode { get; set; } = AiMode.None;
+ public string? AiName { get; set; }
+
///
/// Fires when a position update is received.
///
@@ -87,7 +94,7 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
}
}
- public EntryCar(string model, string? skin, byte sessionId, Func aiStateFactory, SessionManager sessionManager, ACServerConfiguration configuration, EntryCarManager entryCarManager, AiSpline? spline = null)
+ public EntryCar(string model, string? skin, byte sessionId, SessionManager sessionManager, ACServerConfiguration configuration, EntryCarManager entryCarManager)
{
Model = model;
Skin = skin ?? "";
@@ -95,14 +102,8 @@ public EntryCar(string model, string? skin, byte sessionId, Func NetworkDistanceSquared)
@@ -284,7 +242,7 @@ public bool GetPositionUpdateForCar(EntryCar toCar, out PositionUpdateOut positi
}
positionUpdateOut = new PositionUpdateOut(SessionId,
- AiControlled ? AiPakSequenceIds[toCar.SessionId]++ : status.PakSequenceId,
+ status.PakSequenceId,
(uint)(status.Timestamp - toCar.TimeOffset),
Ping,
status.Position,
@@ -327,38 +285,4 @@ public void SetCollisions(bool enable)
Enabled = EnableCollisions
});
}
-
- public bool TryResetPosition()
- {
- if (_spline == null)
- {
- Logger.Information("Failed reset position for {Player} ({SessionId})",Client?.Name, Client?.SessionId);
- return false;
- }
-
- if (_sessionManager.ServerTimeMilliseconds < _sessionManager.CurrentSession.StartTimeMilliseconds + 20_000
- || (_sessionManager.ServerTimeMilliseconds > _sessionManager.CurrentSession.EndTimeMilliseconds
- && _sessionManager.CurrentSession.EndTimeMilliseconds > 0))
- return false;
-
- var (splinePointId, _) = _spline.WorldToSpline(Status.Position);
-
- var splinePoint = _spline.Points[splinePointId];
-
- var position = splinePoint.Position;
- var direction = - _spline.Operations.GetForwardVector(splinePoint.NextId);
-
- SetCollisions(false);
-
- _ = Task.Run(async () =>
- {
- await Task.Delay(500);
- Client?.SendTeleportCarPacket(position, direction);
- await Task.Delay(10000);
- SetCollisions(true);
- });
-
- Logger.Information("Reset position for {Player} ({SessionId})",Client?.Name, Client?.SessionId);
- return true;
- }
}
diff --git a/AssettoServer/Server/EntryCarManager.cs b/AssettoServer/Server/EntryCarManager.cs
index 09f028ad..66b02099 100644
--- a/AssettoServer/Server/EntryCarManager.cs
+++ b/AssettoServer/Server/EntryCarManager.cs
@@ -245,7 +245,6 @@ internal void Initialize()
{
var entry = _configuration.EntryList.Cars[i];
var driverOptions = CSPDriverOptions.Parse(entry.Skin);
- var aiMode = _configuration.Extra.EnableAi ? entry.AiMode : AiMode.None;
var car = _entryCarFactory(entry.Model, entry.Skin, (byte)i);
car.SpectatorMode = entry.SpectatorMode;
@@ -253,9 +252,6 @@ internal void Initialize()
car.Restrictor = entry.Restrictor;
car.FixedSetup = entry.FixedSetup;
car.DriverOptionsFlags = driverOptions;
- car.AiMode = aiMode;
- car.AiEnableColorChanges = driverOptions.HasFlag(DriverOptionsFlags.AllowColorChange);
- car.AiControlled = aiMode != AiMode.None;
car.NetworkDistanceSquared = MathF.Pow(_configuration.Extra.NetworkBubbleDistance, 2);
car.OutsideNetworkBubbleUpdateRateMs = 1000 / _configuration.Extra.OutsideNetworkBubbleRefreshRateHz;
car.LegalTyres = entry.LegalTyres ?? _configuration.Server.LegalTyres;
diff --git a/AssettoServer/Server/Lua/assettoserver.lua b/AssettoServer/Server/Lua/assettoserver.lua
index ac934b9a..15791a38 100644
--- a/AssettoServer/Server/Lua/assettoserver.lua
+++ b/AssettoServer/Server/Lua/assettoserver.lua
@@ -97,14 +97,6 @@ local teleportToPitsEvent = ac.OnlineEvent({
end
end)
-local requestResetCarEvent = ac.OnlineEvent({
- ac.StructItem.key("AS_RequestResetCar"),
- dummy = ac.StructItem.byte(),
-}, function (sender, message)
- if sender ~= nil then return end
- ac.debug("request_reset_car", message.dummy)
-end)
-
local luaReadyEvent = ac.OnlineEvent({
ac.StructItem.key("AS_LuaReady"),
dummy = ac.StructItem.byte()
@@ -278,7 +270,13 @@ end
ui.registerOnlineExtra(ui.Icons.Info, "AssettoServer", function () return true end, window_AssettoServer, nil, ui.OnlineExtraFlags.Tool)
-local resetCarControl = ac.ControlButton('__EXT_CMD_RESET', nil)
-resetCarControl:onPressed(function() requestResetCarEvent({}) end)
+local teleportToPitsEvent = ac.OnlineEvent({
+ ac.StructItem.key("AS_TeleportToPits"),
+ dummy = ac.StructItem.byte()
+}, function (sender, message)
+ if sender.index == 0 and ac.INIConfig.onlineExtras():get("EXTRA_RULES", "NO_BACK_TO_PITS", 0) == 0 then
+ physics.teleportCarTo(0, ac.SpawnSet.Pits)
+ end
+end)
luaReadyEvent({})
diff --git a/AssettoServer/Startup.cs b/AssettoServer/Startup.cs
index ca1cd945..7d302677 100644
--- a/AssettoServer/Startup.cs
+++ b/AssettoServer/Startup.cs
@@ -13,7 +13,6 @@
using AssettoServer.Network.Udp;
using AssettoServer.Server;
using AssettoServer.Server.Admin;
-using AssettoServer.Server.Ai;
using AssettoServer.Server.Blacklist;
using AssettoServer.Server.CMContentProviders;
using AssettoServer.Server.Configuration;
@@ -61,7 +60,6 @@ public void ConfigureContainer(ContainerBuilder builder)
builder.RegisterType().AsSelf().As().SingleInstance();
builder.RegisterType().AsSelf().As().SingleInstance();
builder.RegisterModule(new WeatherModule(_configuration));
- builder.RegisterModule(new AiModule(_configuration));
builder.RegisterType().AsSelf().As().As().SingleInstance();
builder.RegisterType().AsSelf().As().SingleInstance();
//builder.RegisterType().As().SingleInstance();
diff --git a/AutoModerationPlugin/AutoModerationPlugin.cs b/AutoModerationPlugin/AutoModerationPlugin.cs
index 402b172c..95d440fc 100644
--- a/AutoModerationPlugin/AutoModerationPlugin.cs
+++ b/AutoModerationPlugin/AutoModerationPlugin.cs
@@ -1,11 +1,11 @@
using System.Reflection;
using AssettoServer.Network.Tcp;
using AssettoServer.Server;
-using AssettoServer.Server.Ai.Splines;
using AssettoServer.Server.Configuration;
using AssettoServer.Server.Weather;
using Microsoft.Extensions.Hosting;
using Serilog;
+using TrafficAiPlugin.Shared;
namespace AutoModerationPlugin;
@@ -23,7 +23,7 @@ public AutoModerationPlugin(AutoModerationConfiguration configuration,
ACServerConfiguration serverConfiguration,
CSPServerScriptProvider scriptProvider,
Func entryCarAutoModerationFactory,
- AiSpline? aiSpline = null)
+ IAiSpline? aiSpline = null)
{
_configuration = configuration;
_entryCarManager = entryCarManager;
diff --git a/AutoModerationPlugin/AutoModerationPlugin.csproj b/AutoModerationPlugin/AutoModerationPlugin.csproj
index 24cc645f..2a916bc9 100644
--- a/AutoModerationPlugin/AutoModerationPlugin.csproj
+++ b/AutoModerationPlugin/AutoModerationPlugin.csproj
@@ -25,6 +25,9 @@
false
runtime
+
+ all
+
diff --git a/AutoModerationPlugin/EntryCarAutoModeration.cs b/AutoModerationPlugin/EntryCarAutoModeration.cs
index 118c8d35..6aabe3a9 100644
--- a/AutoModerationPlugin/EntryCarAutoModeration.cs
+++ b/AutoModerationPlugin/EntryCarAutoModeration.cs
@@ -1,14 +1,13 @@
using System.Numerics;
using AssettoServer.Network.Tcp;
using AssettoServer.Server;
-using AssettoServer.Server.Ai.Splines;
using AssettoServer.Server.Configuration;
using AssettoServer.Server.Weather;
using AssettoServer.Shared.Network.Packets.Incoming;
using AssettoServer.Shared.Network.Packets.Outgoing;
using AssettoServer.Shared.Network.Packets.Shared;
using AutoModerationPlugin.Packets;
-using Serilog;
+using TrafficAiPlugin.Shared;
namespace AutoModerationPlugin;
@@ -42,7 +41,7 @@ public class EntryCarAutoModeration
private const double NauticalTwilight = -12.0 * Math.PI / 180.0;
private readonly EntryCar _entryCar;
- private readonly AiSpline? _aiSpline;
+ private readonly IAiSpline? _aiSpline;
private readonly ACServerConfiguration _serverConfiguration;
private readonly AutoModerationConfiguration _configuration;
private readonly EntryCarManager _entryCarManager;
@@ -56,7 +55,8 @@ public EntryCarAutoModeration(EntryCar entryCar,
WeatherManager weatherManager,
SessionManager sessionManager,
ACServerConfiguration serverConfiguration,
- AiSpline? aiSpline = null)
+ ITrafficAi? trafficAi = null,
+ IAiSpline? aiSpline = null)
{
_entryCar = entryCar;
_configuration = configuration;
@@ -71,7 +71,8 @@ public EntryCarAutoModeration(EntryCar entryCar,
_entryCar.PositionUpdateReceived += OnPositionUpdateReceived;
}
- _laneRadiusSquared = MathF.Pow(_serverConfiguration.Extra.AiParams.LaneWidthMeters / 2.0f * 1.25f, 2);
+ if (trafficAi != null)
+ _laneRadiusSquared = MathF.Pow(trafficAi.GetLaneWidthMeters() / 2.0f * 1.25f, 2);
}
private void OnPositionUpdateReceived(EntryCar sender, in PositionUpdateIn positionUpdate)
diff --git a/FastTravelPlugin/FastTravelPlugin.cs b/FastTravelPlugin/FastTravelPlugin.cs
index a97a0ae4..6beec147 100644
--- a/FastTravelPlugin/FastTravelPlugin.cs
+++ b/FastTravelPlugin/FastTravelPlugin.cs
@@ -3,23 +3,23 @@
using System.Text.Json;
using AssettoServer.Network.Tcp;
using AssettoServer.Server;
-using AssettoServer.Server.Ai.Splines;
using AssettoServer.Server.Configuration;
using AssettoServer.Utils;
using FastTravelPlugin.Packets;
using Microsoft.Extensions.Hosting;
+using TrafficAiPlugin.Shared;
namespace FastTravelPlugin;
public class FastTravelPlugin : IHostedService
{
- private readonly AiSpline _aiSpline;
+ private readonly IAiSpline _aiSpline;
public FastTravelPlugin(FastTravelConfiguration configuration,
ACServerConfiguration serverConfiguration,
CSPServerScriptProvider scriptProvider,
CSPClientMessageTypeManager cspClientMessageTypeManager,
- AiSpline? aiSpline = null)
+ IAiSpline? aiSpline = null)
{
_aiSpline = aiSpline ?? throw new ConfigurationException("FastTravelPlugin does not work with AI traffic disabled");
diff --git a/FastTravelPlugin/FastTravelPlugin.csproj b/FastTravelPlugin/FastTravelPlugin.csproj
index 981a5c71..59465c75 100644
--- a/FastTravelPlugin/FastTravelPlugin.csproj
+++ b/FastTravelPlugin/FastTravelPlugin.csproj
@@ -25,6 +25,9 @@
false
runtime
+
+ all
+
diff --git a/ReplayPlugin/Data/ReplayFrameState.cs b/ReplayPlugin/Data/ReplayFrameState.cs
index 4a608423..73eef69f 100644
--- a/ReplayPlugin/Data/ReplayFrameState.cs
+++ b/ReplayPlugin/Data/ReplayFrameState.cs
@@ -1,6 +1,6 @@
-using AssettoServer.Server.Ai;
-using AssettoServer.Shared.Model;
+using AssettoServer.Shared.Model;
using Microsoft.Extensions.ObjectPool;
+using TrafficAiPlugin.Shared;
namespace ReplayPlugin.Data;
@@ -8,7 +8,7 @@ public class ReplayFrameState : IResettable
{
public readonly List> PlayerCars = [];
public readonly List> AiCars = [];
- public readonly Dictionary AiStateMapping = [];
+ public readonly Dictionary AiStateMapping = [];
public readonly Dictionary> AiFrameMapping = [];
public bool TryReset()
diff --git a/ReplayPlugin/ReplayPlugin.cs b/ReplayPlugin/ReplayPlugin.cs
index 4c8c454c..df2e9639 100644
--- a/ReplayPlugin/ReplayPlugin.cs
+++ b/ReplayPlugin/ReplayPlugin.cs
@@ -12,6 +12,7 @@
using ReplayPlugin.Data;
using ReplayPlugin.Packets;
using Serilog;
+using TrafficAiPlugin.Shared;
namespace ReplayPlugin;
@@ -25,6 +26,7 @@ public class ReplayPlugin : CriticalBackgroundService, IAssettoServerAutostart
private readonly Summary _onUpdateTimer;
private readonly EntryCarExtraDataManager _extraData;
private readonly ReplayMetadataProvider _metadata;
+ private readonly ITrafficAi? _trafficAi;
public ReplayPlugin(IHostApplicationLifetime applicationLifetime,
EntryCarManager entryCarManager,
@@ -35,7 +37,8 @@ public ReplayPlugin(IHostApplicationLifetime applicationLifetime,
CSPServerScriptProvider scriptProvider,
CSPClientMessageTypeManager cspClientMessageTypeManager,
EntryCarExtraDataManager extraData,
- ReplayMetadataProvider metadata) : base(applicationLifetime)
+ ReplayMetadataProvider metadata,
+ ITrafficAi? trafficAi = null) : base(applicationLifetime)
{
_entryCarManager = entryCarManager;
_weather = weather;
@@ -44,6 +47,7 @@ public ReplayPlugin(IHostApplicationLifetime applicationLifetime,
_configuration = configuration;
_extraData = extraData;
_metadata = metadata;
+ _trafficAi = trafficAi;
_onUpdateTimer = Metrics.CreateSummary("assettoserver_replayplugin_onupdate", "ReplayPlugin.OnUpdate Duration", MetricDefaults.DefaultQuantiles);
@@ -78,16 +82,19 @@ private void Update()
}
else if (entryCar.AiControlled)
{
- for (int i = 0; i < entryCar.LastSeenAiState.Length; i++)
+ if (_trafficAi == null) continue;
+
+ var entryCarAi = _trafficAi.GetAiCarBySessionId(entryCar.SessionId);
+ for (int i = 0; i < entryCarAi.LastSeenAiState.Length; i++)
{
- var aiState = entryCar.LastSeenAiState[i];
+ var aiState = entryCarAi.LastSeenAiState[i];
if (aiState == null) continue;
if (!_state.AiStateMapping.TryGetValue(aiState, out var aiStateId))
{
aiStateId = (short)_state.AiCars.Count;
_state.AiStateMapping.Add(aiState, aiStateId);
- _state.AiCars.Add((aiState.EntryCar.SessionId, aiState.Status));
+ _state.AiCars.Add((aiState.SessionId, aiState.Status));
}
if (_state.AiFrameMapping.TryGetValue((byte)i, out var aiFrameMappingList))
diff --git a/ReplayPlugin/ReplayPlugin.csproj b/ReplayPlugin/ReplayPlugin.csproj
index 5e0e84ce..c2b43c03 100644
--- a/ReplayPlugin/ReplayPlugin.csproj
+++ b/ReplayPlugin/ReplayPlugin.csproj
@@ -25,6 +25,9 @@
false
runtime
+
+ all
+
diff --git a/TrafficAiPlugin.Shared/IAiSpline.cs b/TrafficAiPlugin.Shared/IAiSpline.cs
new file mode 100644
index 00000000..ad04e881
--- /dev/null
+++ b/TrafficAiPlugin.Shared/IAiSpline.cs
@@ -0,0 +1,11 @@
+using System.Numerics;
+using TrafficAiPlugin.Shared.Splines;
+
+namespace TrafficAiPlugin.Shared;
+
+public interface IAiSpline
+{
+ public SplinePointOperations Operations { get; }
+ public ReadOnlySpan Points { get; }
+ public (int PointId, float DistanceSquared) WorldToSpline(Vector3 position);
+}
diff --git a/TrafficAiPlugin.Shared/IAiState.cs b/TrafficAiPlugin.Shared/IAiState.cs
new file mode 100644
index 00000000..48572dcc
--- /dev/null
+++ b/TrafficAiPlugin.Shared/IAiState.cs
@@ -0,0 +1,9 @@
+using AssettoServer.Shared.Model;
+
+namespace TrafficAiPlugin.Shared;
+
+public interface IAiState
+{
+ public CarStatus Status { get; }
+ public byte SessionId { get; }
+}
diff --git a/TrafficAiPlugin.Shared/IEntryCarTrafficAi.cs b/TrafficAiPlugin.Shared/IEntryCarTrafficAi.cs
new file mode 100644
index 00000000..825baa9f
--- /dev/null
+++ b/TrafficAiPlugin.Shared/IEntryCarTrafficAi.cs
@@ -0,0 +1,13 @@
+using AssettoServer.Server;
+
+namespace TrafficAiPlugin.Shared;
+
+public interface IEntryCarTrafficAi
+{
+ public IAiState?[] LastSeenAiState { get; }
+ public EntryCar EntryCar { get; }
+
+ public void SetAiOverbooking(int count);
+ public bool TryResetPosition();
+ public void AiUpdate();
+}
diff --git a/TrafficAiPlugin.Shared/ITrafficAi.cs b/TrafficAiPlugin.Shared/ITrafficAi.cs
new file mode 100644
index 00000000..6b26a063
--- /dev/null
+++ b/TrafficAiPlugin.Shared/ITrafficAi.cs
@@ -0,0 +1,7 @@
+namespace TrafficAiPlugin.Shared;
+
+public interface ITrafficAi
+{
+ public IEntryCarTrafficAi GetAiCarBySessionId(byte sessionId);
+ public float GetLaneWidthMeters();
+}
diff --git a/AssettoServer/Server/Ai/Splines/SplinePoint.cs b/TrafficAiPlugin.Shared/Splines/SplinePoint.cs
similarity index 91%
rename from AssettoServer/Server/Ai/Splines/SplinePoint.cs
rename to TrafficAiPlugin.Shared/Splines/SplinePoint.cs
index b1a78746..6db01aa5 100644
--- a/AssettoServer/Server/Ai/Splines/SplinePoint.cs
+++ b/TrafficAiPlugin.Shared/Splines/SplinePoint.cs
@@ -1,7 +1,7 @@
using System.Numerics;
using System.Runtime.InteropServices;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Shared.Splines;
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SplinePoint
diff --git a/AssettoServer/Server/Ai/Splines/SplinePointOperations.cs b/TrafficAiPlugin.Shared/Splines/SplinePointOperations.cs
similarity index 95%
rename from AssettoServer/Server/Ai/Splines/SplinePointOperations.cs
rename to TrafficAiPlugin.Shared/Splines/SplinePointOperations.cs
index 5d0d4945..b83e897a 100644
--- a/AssettoServer/Server/Ai/Splines/SplinePointOperations.cs
+++ b/TrafficAiPlugin.Shared/Splines/SplinePointOperations.cs
@@ -1,10 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
+using System.Numerics;
using AssettoServer.Utils;
using Serilog;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Shared.Splines;
public readonly ref struct SplinePointOperations
{
diff --git a/TrafficAiPlugin.Shared/TrafficAiPlugin.Shared.csproj b/TrafficAiPlugin.Shared/TrafficAiPlugin.Shared.csproj
new file mode 100644
index 00000000..e8e54e06
--- /dev/null
+++ b/TrafficAiPlugin.Shared/TrafficAiPlugin.Shared.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+ false
+ embedded
+ ..\out-$(RuntimeIdentifier)\plugins\$(MSBuildProjectName)\
+ $(MSBuildProjectDirectory)=$(MSBuildProjectName)
+
+
+
+ false
+ ..\AssettoServer\bin\$(Configuration)\$(TargetFramework)\plugins\$(MSBuildProjectName)
+
+
+
+
+ false
+ runtime
+
+
+ false
+ runtime
+
+
+
+
+
+
+
diff --git a/AssettoServer/Server/Ai/AiBehavior.cs b/TrafficAiPlugin/AiBehavior.cs
similarity index 81%
rename from AssettoServer/Server/Ai/AiBehavior.cs
rename to TrafficAiPlugin/AiBehavior.cs
index 9241fdd6..d8bb234e 100644
--- a/AssettoServer/Server/Ai/AiBehavior.cs
+++ b/TrafficAiPlugin/AiBehavior.cs
@@ -1,27 +1,26 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
+using System.Numerics;
using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
using AssettoServer.Network.Http;
using AssettoServer.Network.Tcp;
-using AssettoServer.Server.Ai.Splines;
+using AssettoServer.Server;
using AssettoServer.Server.Configuration;
using AssettoServer.Shared.Network.Packets.Outgoing;
using AssettoServer.Utils;
using Microsoft.Extensions.Hosting;
using Prometheus;
using Serilog;
+using TrafficAiPlugin.Configuration;
+using TrafficAiPlugin.Splines;
-namespace AssettoServer.Server.Ai;
+namespace TrafficAiPlugin;
public class AiBehavior : BackgroundService
{
- private readonly ACServerConfiguration _configuration;
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly TrafficAiConfiguration _configuration;
private readonly SessionManager _sessionManager;
private readonly EntryCarManager _entryCarManager;
+ private readonly TrafficAi _trafficAi;
private readonly AiSpline _spline;
private readonly HttpInfoCache _httpInfoCache;
@@ -33,24 +32,30 @@ public class AiBehavior : BackgroundService
private readonly Summary _obstacleDetectionDurationTimer;
public AiBehavior(SessionManager sessionManager,
- ACServerConfiguration configuration,
+ ACServerConfiguration serverConfiguration,
+ TrafficAiConfiguration configuration,
EntryCarManager entryCarManager,
- CSPServerScriptProvider serverScriptProvider,
+ CSPServerScriptProvider serverScriptProvider,
+ TrafficAi trafficAi,
AiSpline spline,
HttpInfoCache httpInfoCache)
{
_sessionManager = sessionManager;
+ _serverConfiguration = serverConfiguration;
_configuration = configuration;
_entryCarManager = entryCarManager;
+ _trafficAi = trafficAi;
_spline = spline;
_httpInfoCache = httpInfoCache;
_junctionEvaluator = new JunctionEvaluator(spline, false);
- if (_configuration.Extra.AiParams.Debug)
+ if (_configuration.Debug)
{
- using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("AssettoServer.Server.Ai.ai_debug.lua")!;
- serverScriptProvider.AddScript(stream, "ai_debug.lua");
+ using var aiDebugStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("TrafficAiPlugin.lua.ai_debug.lua")!;
+ serverScriptProvider.AddScript(aiDebugStream, "ai_debug.lua");
}
+ using var resetCarStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("TrafficAiPlugin.lua.resetcar.lua")!;
+ serverScriptProvider.AddScript(resetCarStream, "resetcar.lua");
_updateDurationTimer = Metrics.CreateSummary("assettoserver_aibehavior_update", "AiBehavior.Update Duration", MetricDefaults.DefaultQuantiles);
_obstacleDetectionDurationTimer = Metrics.CreateSummary("assettoserver_aibehavior_obstacledetection", "AiBehavior.ObstacleDetection Duration", MetricDefaults.DefaultQuantiles);
@@ -62,16 +67,17 @@ public AiBehavior(SessionManager sessionManager,
};
_entryCarManager.ClientDisconnected += OnClientDisconnected;
- _configuration.Extra.AiParams.PropertyChanged += (_, _) => AdjustOverbooking();
+ _configuration.PropertyChanged += (_, _) => AdjustOverbooking();
_sessionManager.SessionChanged += OnSessionChanged;
}
- private static void OnCollision(ACTcpClient sender, CollisionEventArgs args)
+ private void OnCollision(ACTcpClient sender, CollisionEventArgs args)
{
if (args.TargetCar?.AiControlled == true)
{
- var targetAiState = args.TargetCar.GetClosestAiState(sender.EntryCar.Status.Position);
+ var target = _trafficAi.GetAiCarBySessionId(args.TargetCar.SessionId);
+ var targetAiState = target.GetClosestAiState(sender.EntryCar.Status.Position);
if (targetAiState.AiState != null && targetAiState.DistanceSquared < 25 * 25)
{
Task.Delay(Random.Shared.Next(100, 500)).ContinueWith(_ => targetAiState.AiState.StopForCollision());
@@ -81,7 +87,8 @@ private static void OnCollision(ACTcpClient sender, CollisionEventArgs args)
private void OnClientChecksumPassed(ACTcpClient sender, EventArgs args)
{
- sender.EntryCar.SetAiControl(false);
+ var entryCar = _trafficAi.GetAiCarBySessionId(sender.SessionId);
+ entryCar.SetAiControl(false);
AdjustOverbooking();
}
@@ -100,11 +107,12 @@ private async Task ObstacleDetectionAsync(CancellationToken stoppingToken)
var entryCar = _entryCarManager.EntryCars[i];
if (entryCar.AiControlled)
{
- entryCar.AiObstacleDetection();
+ var entryCarAi = _trafficAi.GetAiCarBySessionId(entryCar.SessionId);
+ entryCarAi.AiObstacleDetection();
}
}
- if (_configuration.Extra.AiParams.Debug)
+ if (_configuration.Debug)
{
SendDebugPackets();
}
@@ -137,7 +145,8 @@ private void SendDebugPackets()
{
if (!car.AiControlled) continue;
- var (aiState, _) = car.GetClosestAiState(player.Status.Position);
+ var carAi = _trafficAi.GetAiCarBySessionId(car.SessionId);
+ var (aiState, _) = carAi.GetClosestAiState(player.Status.Position);
if (aiState == null) continue;
sessionIds.Add(car.SessionId);
@@ -163,12 +172,12 @@ private void SendDebugPackets()
}
}
- private readonly List _playerCars = new();
+ private readonly List _playerCars = new();
private readonly List _initializedAiStates = new();
private readonly List _uninitializedAiStates = new();
private readonly List _playerOffsetPositions = new();
private readonly List> _aiMinDistanceToPlayer = new();
- private readonly List> _playerMinDistanceToAi = new();
+ private readonly List> _playerMinDistanceToAi = new();
private void Update()
{
using var context = _updateDurationTimer.NewTimer();
@@ -187,15 +196,16 @@ private void Update()
if (!entryCar.AiControlled
&& entryCar.Client?.HasSentFirstUpdate == true
- && _sessionManager.ServerTimeMilliseconds - entryCar.LastActiveTime < _configuration.Extra.AiParams.PlayerAfkTimeoutMilliseconds
- && (_configuration.Extra.AiParams.TwoWayTraffic || _configuration.Extra.AiParams.WrongWayTraffic || drivingTheRightWay))
+ && _sessionManager.ServerTimeMilliseconds - entryCar.LastActiveTime < _configuration.PlayerAfkTimeoutMilliseconds
+ && (_configuration.TwoWayTraffic || _configuration.WrongWayTraffic || drivingTheRightWay))
{
_playerCars.Add(entryCar);
}
else if (entryCar.AiControlled)
{
- entryCar.RemoveUnsafeStates();
- entryCar.GetInitializedStates(_initializedAiStates, _uninitializedAiStates);
+ var entryCarAi = _trafficAi.GetAiCarBySessionId(entryCar.SessionId);
+ entryCarAi.RemoveUnsafeStates();
+ entryCarAi.GetInitializedStates(_initializedAiStates, _uninitializedAiStates);
}
}
@@ -212,7 +222,7 @@ private void Update()
for (int i = 0; i < _playerCars.Count; i++)
{
- _playerMinDistanceToAi.Add(new KeyValuePair(_playerCars[i], float.MaxValue));
+ _playerMinDistanceToAi.Add(new KeyValuePair(_playerCars[i], float.MaxValue));
}
// Get minimum distance to a player for each AI
@@ -226,7 +236,7 @@ private void Update()
var offsetPosition = _playerCars[j].Status.Position;
if (_playerCars[j].Status.Velocity != Vector3.Zero)
{
- offsetPosition += Vector3.Normalize(_playerCars[j].Status.Velocity) * _configuration.Extra.AiParams.PlayerPositionOffsetMeters;
+ offsetPosition += Vector3.Normalize(_playerCars[j].Status.Velocity) * _configuration.PlayerPositionOffsetMeters;
}
_playerOffsetPositions.Add(offsetPosition);
@@ -241,7 +251,7 @@ private void Update()
if (_playerMinDistanceToAi[j].Value > distanceSquared)
{
- _playerMinDistanceToAi[j] = new KeyValuePair(_playerCars[j], distanceSquared);
+ _playerMinDistanceToAi[j] = new KeyValuePair(_playerCars[j], distanceSquared);
}
}
}
@@ -251,7 +261,7 @@ private void Update()
foreach (var dist in _aiMinDistanceToPlayer)
{
- if (dist.Value > _configuration.Extra.AiParams.PlayerRadiusSquared
+ if (dist.Value > _configuration.PlayerRadiusSquared
&& _sessionManager.ServerTimeMilliseconds > dist.Key.SpawnProtectionEnds)
{
_uninitializedAiStates.Add(dist.Key);
@@ -329,7 +339,7 @@ private void Update()
private async Task UpdateAsync(CancellationToken stoppingToken)
{
- using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_configuration.Extra.AiParams.AiBehaviorUpdateIntervalMilliseconds));
+ using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_configuration.AiBehaviorUpdateIntervalMilliseconds));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
@@ -348,7 +358,8 @@ private void OnClientDisconnected(ACTcpClient sender, EventArgs args)
{
if (sender.EntryCar.AiMode != AiMode.None)
{
- sender.EntryCar.SetAiControl(true);
+ var entryCarAi = _trafficAi.GetAiCarBySessionId(sender.SessionId);
+ entryCarAi.SetAiControl(true);
AdjustOverbooking();
}
}
@@ -388,13 +399,14 @@ private bool IsPositionSafe(int pointId)
for (var i = 0; i < _entryCarManager.EntryCars.Length; i++)
{
var entryCar = _entryCarManager.EntryCars[i];
- if (entryCar.AiControlled && !entryCar.IsPositionSafe(pointId))
+ var entryCarAi = _trafficAi.GetAiCarBySessionId(entryCar.SessionId);
+ if (entryCar.AiControlled && !entryCarAi.IsPositionSafe(pointId))
{
return false;
}
if (entryCar.Client?.HasSentFirstUpdate == true
- && Vector3.DistanceSquared(entryCar.Status.Position, ops.Points[pointId].Position) < _configuration.Extra.AiParams.SpawnSafetyDistanceToPlayerSquared)
+ && Vector3.DistanceSquared(entryCar.Status.Position, ops.Points[pointId].Position) < _configuration.SpawnSafetyDistanceToPlayerSquared)
{
return false;
}
@@ -414,12 +426,12 @@ private int GetSpawnPoint(EntryCar playerCar)
int direction = Vector3.Dot(ops.GetForwardVector(result.PointId), playerCar.Status.Velocity) > 0 ? 1 : -1;
// Do not not spawn if a player is too far away from the AI spline, e.g. in pits or in a part of the map without traffic
- if (result.DistanceSquared > _configuration.Extra.AiParams.MaxPlayerDistanceToAiSplineSquared)
+ if (result.DistanceSquared > _configuration.MaxPlayerDistanceToAiSplineSquared)
{
return -1;
}
- int spawnDistance = Random.Shared.Next(_configuration.Extra.AiParams.MinSpawnDistancePoints, _configuration.Extra.AiParams.MaxSpawnDistancePoints);
+ int spawnDistance = Random.Shared.Next(_configuration.MinSpawnDistancePoints, _configuration.MaxSpawnDistancePoints);
var spawnPointId = _junctionEvaluator.Traverse(result.PointId, spawnDistance * direction);
if (spawnPointId >= 0)
@@ -456,7 +468,7 @@ private void AdjustOverbooking()
return;
}
- int targetAiCount = Math.Min(playerCount * Math.Min((int)Math.Round(_configuration.Extra.AiParams.AiPerPlayerTargetCount * _configuration.Extra.AiParams.TrafficDensity), aiSlots.Count), _configuration.Extra.AiParams.MaxAiTargetCount);
+ int targetAiCount = Math.Min(playerCount * Math.Min((int)Math.Round(_configuration.AiPerPlayerTargetCount * _configuration.TrafficDensity), aiSlots.Count), _configuration.MaxAiTargetCount);
int overbooking = targetAiCount / aiSlots.Count;
int rest = targetAiCount % aiSlots.Count;
@@ -466,7 +478,8 @@ private void AdjustOverbooking()
for (int i = 0; i < aiSlots.Count; i++)
{
- aiSlots[i].SetAiOverbooking(i < rest ? overbooking + 1 : overbooking);
+ var entryCarAi = _trafficAi.GetAiCarBySessionId(aiSlots[i].SessionId);
+ entryCarAi.SetAiOverbooking(i < rest ? overbooking + 1 : overbooking);
}
}
diff --git a/AssettoServer/Server/OpenSlotFilters/AiSlotFilter.cs b/TrafficAiPlugin/AiSlotFilter.cs
similarity index 51%
rename from AssettoServer/Server/OpenSlotFilters/AiSlotFilter.cs
rename to TrafficAiPlugin/AiSlotFilter.cs
index 141ef829..415aa45a 100644
--- a/AssettoServer/Server/OpenSlotFilters/AiSlotFilter.cs
+++ b/TrafficAiPlugin/AiSlotFilter.cs
@@ -1,14 +1,15 @@
-using System.Threading.Tasks;
-using AssettoServer.Server.Configuration;
+using AssettoServer.Server;
+using AssettoServer.Server.OpenSlotFilters;
+using TrafficAiPlugin.Configuration;
-namespace AssettoServer.Server.OpenSlotFilters;
+namespace TrafficAiPlugin;
public class AiSlotFilter : OpenSlotFilterBase
{
private readonly EntryCarManager _entryCarManager;
- private readonly ACServerConfiguration _configuration;
+ private readonly TrafficAiConfiguration _configuration;
- public AiSlotFilter(EntryCarManager entryCarManager, ACServerConfiguration configuration)
+ public AiSlotFilter(EntryCarManager entryCarManager, TrafficAiConfiguration configuration)
{
_entryCarManager = entryCarManager;
_configuration = configuration;
@@ -17,7 +18,7 @@ public AiSlotFilter(EntryCarManager entryCarManager, ACServerConfiguration confi
public override async ValueTask IsSlotOpen(EntryCar entryCar, ulong guid)
{
if (entryCar.AiMode == AiMode.Fixed
- || (_configuration.Extra.AiParams.MaxPlayerCount > 0 && _entryCarManager.ConnectedCars.Count >= _configuration.Extra.AiParams.MaxPlayerCount))
+ || (_configuration.MaxPlayerCount > 0 && _entryCarManager.ConnectedCars.Count >= _configuration.MaxPlayerCount))
{
return false;
}
diff --git a/AssettoServer/Server/Ai/AiState.cs b/TrafficAiPlugin/AiState.cs
similarity index 83%
rename from AssettoServer/Server/Ai/AiState.cs
rename to TrafficAiPlugin/AiState.cs
index ce394397..a78105e5 100644
--- a/AssettoServer/Server/Ai/AiState.cs
+++ b/TrafficAiPlugin/AiState.cs
@@ -1,10 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
+using System.Drawing;
using System.Numerics;
-using AssettoServer.Server.Ai.Splines;
+using AssettoServer.Server;
using AssettoServer.Server.Configuration;
-using AssettoServer.Server.Configuration.Extra;
using AssettoServer.Server.Weather;
using AssettoServer.Shared.Model;
using AssettoServer.Shared.Network.Packets.Outgoing;
@@ -12,11 +9,16 @@
using JPBotelho;
using Serilog;
using SunCalcNet.Model;
+using TrafficAiPlugin.Configuration;
+using TrafficAiPlugin.Shared;
+using TrafficAiPlugin.Shared.Splines;
+using TrafficAiPlugin.Splines;
-namespace AssettoServer.Server.Ai;
+namespace TrafficAiPlugin;
-public class AiState : IDisposable
+public class AiState : IAiState, IDisposable
{
+ public byte SessionId => EntryCarAi.EntryCar.SessionId;
public CarStatus Status { get; } = new();
public bool Initialized { get; private set; }
@@ -43,7 +45,7 @@ private set
public Color Color { get; private set; }
public byte SpawnCounter { get; private set; }
public float ClosestAiObstacleDistance { get; private set; }
- public EntryCar EntryCar { get; }
+ public EntryCarTrafficAi EntryCarAi { get; }
private const float WalkingSpeed = 10 / 3.6f;
@@ -66,7 +68,8 @@ private set
private float _minObstacleDistance;
private double _randomTwilight;
- private readonly ACServerConfiguration _configuration;
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly TrafficAiConfiguration _configuration;
private readonly SessionManager _sessionManager;
private readonly EntryCarManager _entryCarManager;
private readonly WeatherManager _weatherManager;
@@ -95,11 +98,18 @@ private set
Color.FromArgb(18, 46, 43)
];
- public AiState(EntryCar entryCar, SessionManager sessionManager, WeatherManager weatherManager, ACServerConfiguration configuration, EntryCarManager entryCarManager, AiSpline spline)
+ public AiState(EntryCarTrafficAi entryCarAi,
+ SessionManager sessionManager,
+ WeatherManager weatherManager,
+ ACServerConfiguration serverConfiguration,
+ TrafficAiConfiguration configuration,
+ EntryCarManager entryCarManager,
+ AiSpline spline)
{
- EntryCar = entryCar;
+ EntryCarAi = entryCarAi;
_sessionManager = sessionManager;
_weatherManager = weatherManager;
+ _serverConfiguration = serverConfiguration;
_configuration = configuration;
_entryCarManager = entryCarManager;
_spline = spline;
@@ -127,14 +137,14 @@ public void Despawn()
private void SetRandomSpeed()
{
- float variation = _configuration.Extra.AiParams.MaxSpeedMs * _configuration.Extra.AiParams.MaxSpeedVariationPercent;
+ float variation = _configuration.MaxSpeedMs * _configuration.MaxSpeedVariationPercent;
float fastLaneOffset = 0;
if (_spline.Points[CurrentSplinePointId].LeftId >= 0)
{
- fastLaneOffset = _configuration.Extra.AiParams.RightLaneOffsetMs;
+ fastLaneOffset = _configuration.RightLaneOffsetMs;
}
- InitialMaxSpeed = _configuration.Extra.AiParams.MaxSpeedMs + fastLaneOffset - (variation / 2) + (float)Random.Shared.NextDouble() * variation;
+ InitialMaxSpeed = _configuration.MaxSpeedMs + fastLaneOffset - (variation / 2) + (float)Random.Shared.NextDouble() * variation;
CurrentSpeed = InitialMaxSpeed;
TargetSpeed = InitialMaxSpeed;
MaxSpeed = InitialMaxSpeed;
@@ -159,22 +169,22 @@ public void Teleport(int pointId)
SetRandomSpeed();
SetRandomColor();
- var minDist = _configuration.Extra.AiParams.MinAiSafetyDistanceSquared;
- var maxDist = _configuration.Extra.AiParams.MaxAiSafetyDistanceSquared;
- if (_configuration.Extra.AiParams.LaneCountSpecificOverrides.TryGetValue(_spline.GetLanes(CurrentSplinePointId).Length, out var overrides))
+ var minDist = _configuration.MinAiSafetyDistanceSquared;
+ var maxDist = _configuration.MaxAiSafetyDistanceSquared;
+ if (_configuration.LaneCountSpecificOverrides.TryGetValue(_spline.GetLanes(CurrentSplinePointId).Length, out var overrides))
{
minDist = overrides.MinAiSafetyDistanceSquared;
maxDist = overrides.MaxAiSafetyDistanceSquared;
}
- if (EntryCar.MinAiSafetyDistanceMetersSquared.HasValue)
- minDist = EntryCar.MinAiSafetyDistanceMetersSquared.Value;
- if (EntryCar.MaxAiSafetyDistanceMetersSquared.HasValue)
- maxDist = EntryCar.MaxAiSafetyDistanceMetersSquared.Value;
-
- SpawnProtectionEnds = _sessionManager.ServerTimeMilliseconds + Random.Shared.Next(EntryCar.AiMinSpawnProtectionTimeMilliseconds, EntryCar.AiMaxSpawnProtectionTimeMilliseconds);
- SafetyDistanceSquared = Random.Shared.Next((int)Math.Round(minDist * (1.0f / _configuration.Extra.AiParams.TrafficDensity)),
- (int)Math.Round(maxDist * (1.0f / _configuration.Extra.AiParams.TrafficDensity)));
+ if (EntryCarAi.MinAiSafetyDistanceMetersSquared.HasValue)
+ minDist = EntryCarAi.MinAiSafetyDistanceMetersSquared.Value;
+ if (EntryCarAi.MaxAiSafetyDistanceMetersSquared.HasValue)
+ maxDist = EntryCarAi.MaxAiSafetyDistanceMetersSquared.Value;
+
+ SpawnProtectionEnds = _sessionManager.ServerTimeMilliseconds + Random.Shared.Next(EntryCarAi.AiMinSpawnProtectionTimeMilliseconds, EntryCarAi.AiMaxSpawnProtectionTimeMilliseconds);
+ SafetyDistanceSquared = Random.Shared.Next((int)Math.Round(minDist * (1.0f / _configuration.TrafficDensity)),
+ (int)Math.Round(maxDist * (1.0f / _configuration.TrafficDensity)));
_stoppedForCollisionUntil = 0;
_ignoreObstaclesUntil = 0;
_obstacleHonkEnd = 0;
@@ -279,7 +289,7 @@ public bool CanSpawn(int spawnPointId, AiState? previousAi, AiState? nextAi)
if (!IsKeepingSafetyDistances(in spawnPoint, previousAi, nextAi))
return false;
- return EntryCar.CanSpawnAiState(spawnPoint.Position, this);
+ return EntryCarAi.CanSpawnAiState(spawnPoint.Position, this);
}
private bool IsKeepingSafetyDistances(in SplinePoint spawnPoint, AiState? previousAi, AiState? nextAi)
@@ -287,8 +297,8 @@ private bool IsKeepingSafetyDistances(in SplinePoint spawnPoint, AiState? previo
if (previousAi != null)
{
var distance = MathF.Max(0, Vector3.Distance(spawnPoint.Position, previousAi.Status.Position)
- - previousAi.EntryCar.VehicleLengthPreMeters
- - EntryCar.VehicleLengthPostMeters);
+ - previousAi.EntryCarAi.VehicleLengthPreMeters
+ - EntryCarAi.VehicleLengthPostMeters);
var distanceSquared = distance * distance;
if (distanceSquared < previousAi.SafetyDistanceSquared || distanceSquared < SafetyDistanceSquared)
@@ -298,8 +308,8 @@ private bool IsKeepingSafetyDistances(in SplinePoint spawnPoint, AiState? previo
if (nextAi != null)
{
var distance = MathF.Max(0, Vector3.Distance(spawnPoint.Position, nextAi.Status.Position)
- - nextAi.EntryCar.VehicleLengthPostMeters
- - EntryCar.VehicleLengthPreMeters);
+ - nextAi.EntryCarAi.VehicleLengthPostMeters
+ - EntryCarAi.VehicleLengthPreMeters);
var distanceSquared = distance * distance;
if (distanceSquared < nextAi.SafetyDistanceSquared || distanceSquared < SafetyDistanceSquared)
@@ -312,9 +322,9 @@ private bool IsKeepingSafetyDistances(in SplinePoint spawnPoint, AiState? previo
private bool IsAllowedLaneCount(int spawnPointId)
{
var laneCount = _spline.GetLanes(spawnPointId).Length;
- if (EntryCar.MinLaneCount.HasValue && laneCount < EntryCar.MinLaneCount.Value)
+ if (EntryCarAi.MinLaneCount.HasValue && laneCount < EntryCarAi.MinLaneCount.Value)
return false;
- if (EntryCar.MaxLaneCount.HasValue && laneCount > EntryCar.MaxLaneCount.Value)
+ if (EntryCarAi.MaxLaneCount.HasValue && laneCount > EntryCarAi.MaxLaneCount.Value)
return false;
return true;
@@ -323,11 +333,11 @@ private bool IsAllowedLaneCount(int spawnPointId)
private bool IsAllowedLane(in SplinePoint spawnPoint)
{
var isAllowedLane = true;
- if (EntryCar.AiAllowedLanes != null)
+ if (EntryCarAi.AiAllowedLanes != null)
{
- isAllowedLane = (EntryCar.AiAllowedLanes.Contains(LaneSpawnBehavior.Middle) && spawnPoint.LeftId >= 0 && spawnPoint.RightId >= 0)
- || (EntryCar.AiAllowedLanes.Contains(LaneSpawnBehavior.Left) && spawnPoint.LeftId < 0)
- || (EntryCar.AiAllowedLanes.Contains(LaneSpawnBehavior.Right) && spawnPoint.RightId < 0);
+ isAllowedLane = (EntryCarAi.AiAllowedLanes.Contains(LaneSpawnBehavior.Middle) && spawnPoint.LeftId >= 0 && spawnPoint.RightId >= 0)
+ || (EntryCarAi.AiAllowedLanes.Contains(LaneSpawnBehavior.Left) && spawnPoint.LeftId < 0)
+ || (EntryCarAi.AiAllowedLanes.Contains(LaneSpawnBehavior.Right) && spawnPoint.RightId < 0);
}
return isAllowedLane;
@@ -338,7 +348,7 @@ private bool IsAllowedLane(in SplinePoint spawnPoint)
var points = _spline.Points;
var junctions = _spline.Junctions;
- float maxBrakingDistance = PhysicsUtils.CalculateBrakingDistance(CurrentSpeed, EntryCar.AiDeceleration) * 2 + 20;
+ float maxBrakingDistance = PhysicsUtils.CalculateBrakingDistance(CurrentSpeed, EntryCarAi.AiDeceleration) * 2 + 20;
AiState? closestAiState = null;
float closestAiStateDistance = float.MaxValue;
bool junctionFound = false;
@@ -377,18 +387,18 @@ private bool IsAllowedLane(in SplinePoint spawnPoint)
{
closestAiState = slowest;
closestAiStateDistance = MathF.Max(0, Vector3.Distance(Status.Position, closestAiState.Status.Position)
- - EntryCar.VehicleLengthPreMeters
- - closestAiState.EntryCar.VehicleLengthPostMeters);
+ - EntryCarAi.VehicleLengthPreMeters
+ - closestAiState.EntryCarAi.VehicleLengthPostMeters);
}
}
- float maxCorneringSpeedSquared = PhysicsUtils.CalculateMaxCorneringSpeedSquared(point.Radius, EntryCar.AiCorneringSpeedFactor);
+ float maxCorneringSpeedSquared = PhysicsUtils.CalculateMaxCorneringSpeedSquared(point.Radius, EntryCarAi.AiCorneringSpeedFactor);
if (maxCorneringSpeedSquared < currentSpeedSquared)
{
float maxCorneringSpeed = MathF.Sqrt(maxCorneringSpeedSquared);
float brakingDistance = PhysicsUtils.CalculateBrakingDistance(CurrentSpeed - maxCorneringSpeed,
- EntryCar.AiDeceleration * EntryCar.AiCorneringBrakeForceFactor)
- * EntryCar.AiCorneringBrakeDistanceFactor;
+ EntryCarAi.AiDeceleration * EntryCarAi.AiCorneringBrakeForceFactor)
+ * EntryCarAi.AiCorneringBrakeDistanceFactor;
if (brakingDistance > distanceTravelled)
{
@@ -402,9 +412,9 @@ private bool IsAllowedLane(in SplinePoint spawnPoint)
private bool ShouldIgnorePlayerObstacles()
{
- if (_configuration.Extra.AiParams.IgnorePlayerObstacleSpheres != null)
+ if (_configuration.IgnorePlayerObstacleSpheres != null)
{
- foreach (var sphere in _configuration.Extra.AiParams.IgnorePlayerObstacleSpheres)
+ foreach (var sphere in _configuration.IgnorePlayerObstacleSpheres)
{
if (Vector3.DistanceSquared(Status.Position, sphere.Center) < sphere.RadiusMeters * sphere.RadiusMeters)
{
@@ -521,7 +531,7 @@ public void DetectObstacles()
}
if ((playerSpeed < CurrentSpeed || playerSpeed == 0)
- && playerObstacle.distance < PhysicsUtils.CalculateBrakingDistance(CurrentSpeed - playerSpeed, EntryCar.AiDeceleration) * 2 + 20)
+ && playerObstacle.distance < PhysicsUtils.CalculateBrakingDistance(CurrentSpeed - playerSpeed, EntryCarAi.AiDeceleration) * 2 + 20)
{
targetSpeed = Math.Max(WalkingSpeed, playerSpeed);
hasObstacle = true;
@@ -531,7 +541,7 @@ public void DetectObstacles()
{
float closestTargetSpeed = Math.Min(splineLookahead.ClosestAiState.CurrentSpeed, splineLookahead.ClosestAiState.TargetSpeed);
if ((closestTargetSpeed < CurrentSpeed || splineLookahead.ClosestAiState.CurrentSpeed == 0)
- && splineLookahead.ClosestAiStateDistance < PhysicsUtils.CalculateBrakingDistance(CurrentSpeed - closestTargetSpeed, EntryCar.AiDeceleration) * 2 + 20)
+ && splineLookahead.ClosestAiStateDistance < PhysicsUtils.CalculateBrakingDistance(CurrentSpeed - closestTargetSpeed, EntryCarAi.AiDeceleration) * 2 + 20)
{
targetSpeed = Math.Max(WalkingSpeed, closestTargetSpeed);
hasObstacle = true;
@@ -546,34 +556,34 @@ public void DetectObstacles()
_stoppedForObstacleSince = _sessionManager.ServerTimeMilliseconds;
_obstacleHonkStart = _stoppedForObstacleSince + Random.Shared.Next(3000, 7000);
_obstacleHonkEnd = _obstacleHonkStart + Random.Shared.Next(500, 1500);
- Log.Verbose("AI {SessionId} stopped for obstacle", EntryCar.SessionId);
+ Log.Verbose("AI {SessionId} stopped for obstacle", EntryCarAi.EntryCar.SessionId);
}
else if (CurrentSpeed > 0 && _stoppedForObstacle)
{
_stoppedForObstacle = false;
- Log.Verbose("AI {SessionId} no longer stopped for obstacle", EntryCar.SessionId);
+ Log.Verbose("AI {SessionId} no longer stopped for obstacle", EntryCarAi.EntryCar.SessionId);
}
- else if (_stoppedForObstacle && _sessionManager.ServerTimeMilliseconds - _stoppedForObstacleSince > _configuration.Extra.AiParams.IgnoreObstaclesAfterMilliseconds)
+ else if (_stoppedForObstacle && _sessionManager.ServerTimeMilliseconds - _stoppedForObstacleSince > _configuration.IgnoreObstaclesAfterMilliseconds)
{
_ignoreObstaclesUntil = _sessionManager.ServerTimeMilliseconds + 10_000;
- Log.Verbose("AI {SessionId} ignoring obstacles until {IgnoreObstaclesUntil}", EntryCar.SessionId, _ignoreObstaclesUntil);
+ Log.Verbose("AI {SessionId} ignoring obstacles until {IgnoreObstaclesUntil}", EntryCarAi.EntryCar.SessionId, _ignoreObstaclesUntil);
}
- float deceleration = EntryCar.AiDeceleration;
+ float deceleration = EntryCarAi.AiDeceleration;
if (!hasObstacle)
{
- deceleration *= EntryCar.AiCorneringBrakeForceFactor;
+ deceleration *= EntryCarAi.AiCorneringBrakeForceFactor;
}
MaxSpeed = maxSpeed;
- SetTargetSpeed(targetSpeed, deceleration, EntryCar.AiAcceleration);
+ SetTargetSpeed(targetSpeed, deceleration, EntryCarAi.AiAcceleration);
}
public void StopForCollision()
{
if (!ShouldIgnorePlayerObstacles())
{
- _stoppedForCollisionUntil = _sessionManager.ServerTimeMilliseconds + Random.Shared.Next(EntryCar.AiMinCollisionStopTimeMilliseconds, EntryCar.AiMaxCollisionStopTimeMilliseconds);
+ _stoppedForCollisionUntil = _sessionManager.ServerTimeMilliseconds + Random.Shared.Next(EntryCarAi.AiMinCollisionStopTimeMilliseconds, EntryCarAi.AiMaxCollisionStopTimeMilliseconds);
}
}
@@ -610,7 +620,7 @@ private void SetTargetSpeed(float speed, float deceleration, float acceleration)
private void SetTargetSpeed(float speed)
{
- SetTargetSpeed(speed, EntryCar.AiDeceleration, EntryCar.AiAcceleration);
+ SetTargetSpeed(speed, EntryCarAi.AiDeceleration, EntryCarAi.AiAcceleration);
}
public void Update()
@@ -638,7 +648,7 @@ public void Update()
float moveMeters = (dt / 1000.0f) * CurrentSpeed;
if (!Move(_currentVecProgress + moveMeters) || !_junctionEvaluator.TryNext(CurrentSplinePointId, out var nextPoint))
{
- Log.Debug("Car {SessionId} reached spline end, despawning", EntryCar.SessionId);
+ Log.Debug("Car {SessionId} reached spline end, despawning", EntryCarAi.EntryCar.SessionId);
Despawn();
return;
}
@@ -656,11 +666,11 @@ public void Update()
Z = ops.GetCamber(CurrentSplinePointId, _currentVecProgress / _currentVecLength)
};
- float tyreAngularSpeed = GetTyreAngularSpeed(CurrentSpeed, EntryCar.TyreDiameterMeters);
+ float tyreAngularSpeed = GetTyreAngularSpeed(CurrentSpeed, EntryCarAi.TyreDiameterMeters);
byte encodedTyreAngularSpeed = (byte) (Math.Clamp(MathF.Round(MathF.Log10(tyreAngularSpeed + 1.0f) * 20.0f) * Math.Sign(tyreAngularSpeed), -100.0f, 154.0f) + 100.0f);
Status.Timestamp = _sessionManager.ServerTimeMilliseconds;
- Status.Position = smoothPos.Position with { Y = smoothPos.Position.Y + EntryCar.AiSplineHeightOffsetMeters };
+ Status.Position = smoothPos.Position with { Y = smoothPos.Position.Y + EntryCarAi.AiSplineHeightOffsetMeters };
Status.Rotation = rotation;
Status.Velocity = smoothPos.Tangent * CurrentSpeed;
Status.SteerAngle = 127;
@@ -669,8 +679,8 @@ public void Update()
Status.TyreAngularSpeed[1] = encodedTyreAngularSpeed;
Status.TyreAngularSpeed[2] = encodedTyreAngularSpeed;
Status.TyreAngularSpeed[3] = encodedTyreAngularSpeed;
- Status.EngineRpm = (ushort)MathUtils.Lerp(EntryCar.AiIdleEngineRpm, EntryCar.AiMaxEngineRpm, CurrentSpeed / _configuration.Extra.AiParams.MaxSpeedMs);
- Status.StatusFlag = GetLights(_configuration.Extra.AiParams.EnableDaytimeLights, _weatherManager.CurrentSunPosition, _randomTwilight)
+ Status.EngineRpm = (ushort)MathUtils.Lerp(EntryCarAi.AiIdleEngineRpm, EntryCarAi.AiMaxEngineRpm, CurrentSpeed / _configuration.MaxSpeedMs);
+ Status.StatusFlag = GetLights(_configuration.EnableDaytimeLights, _weatherManager.CurrentSunPosition, _randomTwilight)
| (_sessionManager.ServerTimeMilliseconds < _stoppedForCollisionUntil || CurrentSpeed < 20 / 3.6f ? CarStatusFlags.HazardsOn : 0)
| (CurrentSpeed == 0 || Acceleration < 0 ? CarStatusFlags.BrakeLightsOn : 0)
| (_stoppedForObstacle && _sessionManager.ServerTimeMilliseconds > _obstacleHonkStart && _sessionManager.ServerTimeMilliseconds < _obstacleHonkEnd ? CarStatusFlags.Horn : 0)
diff --git a/AssettoServer/Server/Configuration/Extra/CarSpecificOverrides.cs b/TrafficAiPlugin/Configuration/CarSpecificOverrides.cs
similarity index 97%
rename from AssettoServer/Server/Configuration/Extra/CarSpecificOverrides.cs
rename to TrafficAiPlugin/Configuration/CarSpecificOverrides.cs
index b4d73559..e041cd10 100644
--- a/AssettoServer/Server/Configuration/Extra/CarSpecificOverrides.cs
+++ b/TrafficAiPlugin/Configuration/CarSpecificOverrides.cs
@@ -1,8 +1,7 @@
-using System.Collections.Generic;
using JetBrains.Annotations;
using YamlDotNet.Serialization;
-namespace AssettoServer.Server.Configuration.Extra;
+namespace TrafficAiPlugin.Configuration;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
public class CarSpecificOverrides
diff --git a/AssettoServer/Server/Ai/Configuration/Indicator.cs b/TrafficAiPlugin/Configuration/Indicator.cs
similarity index 52%
rename from AssettoServer/Server/Ai/Configuration/Indicator.cs
rename to TrafficAiPlugin/Configuration/Indicator.cs
index c0da2664..21d1a0d2 100644
--- a/AssettoServer/Server/Ai/Configuration/Indicator.cs
+++ b/TrafficAiPlugin/Configuration/Indicator.cs
@@ -1,4 +1,4 @@
-namespace AssettoServer.Server.Ai.Configuration;
+namespace TrafficAiPlugin.Configuration;
public enum Indicator
{
diff --git a/AssettoServer/Server/Ai/Configuration/JunctionRecord.cs b/TrafficAiPlugin/Configuration/JunctionRecord.cs
similarity index 92%
rename from AssettoServer/Server/Ai/Configuration/JunctionRecord.cs
rename to TrafficAiPlugin/Configuration/JunctionRecord.cs
index 9d734328..d484ec71 100644
--- a/AssettoServer/Server/Ai/Configuration/JunctionRecord.cs
+++ b/TrafficAiPlugin/Configuration/JunctionRecord.cs
@@ -1,6 +1,6 @@
using JetBrains.Annotations;
-namespace AssettoServer.Server.Ai.Configuration;
+namespace TrafficAiPlugin.Configuration;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
public class JunctionRecord
diff --git a/AssettoServer/Server/Configuration/Extra/LaneCountSpecificOverrides.cs b/TrafficAiPlugin/Configuration/LaneCountSpecificOverrides.cs
similarity index 92%
rename from AssettoServer/Server/Configuration/Extra/LaneCountSpecificOverrides.cs
rename to TrafficAiPlugin/Configuration/LaneCountSpecificOverrides.cs
index 255447fa..65c6acc2 100644
--- a/AssettoServer/Server/Configuration/Extra/LaneCountSpecificOverrides.cs
+++ b/TrafficAiPlugin/Configuration/LaneCountSpecificOverrides.cs
@@ -1,7 +1,7 @@
using JetBrains.Annotations;
using YamlDotNet.Serialization;
-namespace AssettoServer.Server.Configuration.Extra;
+namespace TrafficAiPlugin.Configuration;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
public class LaneCountSpecificOverrides
diff --git a/AssettoServer/Server/Configuration/Extra/LaneSpawnBehavior.cs b/TrafficAiPlugin/Configuration/LaneSpawnBehavior.cs
similarity index 56%
rename from AssettoServer/Server/Configuration/Extra/LaneSpawnBehavior.cs
rename to TrafficAiPlugin/Configuration/LaneSpawnBehavior.cs
index aa936060..1deaffa4 100644
--- a/AssettoServer/Server/Configuration/Extra/LaneSpawnBehavior.cs
+++ b/TrafficAiPlugin/Configuration/LaneSpawnBehavior.cs
@@ -1,4 +1,4 @@
-namespace AssettoServer.Server.Configuration.Extra;
+namespace TrafficAiPlugin.Configuration;
public enum LaneSpawnBehavior
{
diff --git a/AssettoServer/Server/Configuration/Extra/Sphere.cs b/TrafficAiPlugin/Configuration/Sphere.cs
similarity index 82%
rename from AssettoServer/Server/Configuration/Extra/Sphere.cs
rename to TrafficAiPlugin/Configuration/Sphere.cs
index fedaa6d8..5421eb57 100644
--- a/AssettoServer/Server/Configuration/Extra/Sphere.cs
+++ b/TrafficAiPlugin/Configuration/Sphere.cs
@@ -1,7 +1,7 @@
using System.Numerics;
using JetBrains.Annotations;
-namespace AssettoServer.Server.Configuration.Extra;
+namespace TrafficAiPlugin.Configuration;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
public class Sphere
diff --git a/AssettoServer/Server/Ai/Configuration/SplineConfiguration.cs b/TrafficAiPlugin/Configuration/SplineConfiguration.cs
similarity index 80%
rename from AssettoServer/Server/Ai/Configuration/SplineConfiguration.cs
rename to TrafficAiPlugin/Configuration/SplineConfiguration.cs
index 259ed49a..abf6235a 100644
--- a/AssettoServer/Server/Ai/Configuration/SplineConfiguration.cs
+++ b/TrafficAiPlugin/Configuration/SplineConfiguration.cs
@@ -1,7 +1,6 @@
-using System.Collections.Generic;
-using JetBrains.Annotations;
+using JetBrains.Annotations;
-namespace AssettoServer.Server.Ai.Configuration;
+namespace TrafficAiPlugin.Configuration;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
public class SplineConfiguration
diff --git a/AssettoServer/Server/Configuration/Extra/AiParams.cs b/TrafficAiPlugin/Configuration/TrafficAIConfiguration.cs
similarity index 92%
rename from AssettoServer/Server/Configuration/Extra/AiParams.cs
rename to TrafficAiPlugin/Configuration/TrafficAIConfiguration.cs
index 17374258..54af891c 100644
--- a/AssettoServer/Server/Configuration/Extra/AiParams.cs
+++ b/TrafficAiPlugin/Configuration/TrafficAIConfiguration.cs
@@ -1,14 +1,17 @@
-using System.Collections.Generic;
+using AssettoServer.Server;
+using AssettoServer.Server.Configuration;
using CommunityToolkit.Mvvm.ComponentModel;
using JetBrains.Annotations;
using YamlDotNet.Serialization;
-namespace AssettoServer.Server.Configuration.Extra;
+namespace TrafficAiPlugin.Configuration;
#pragma warning disable CS0657
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-public partial class AiParams : ObservableObject
+public partial class TrafficAiConfiguration : ObservableObject, IValidateConfiguration
{
+ [YamlMember(Description = "Enable usage of /resetcar to teleport the player to the closest spline point. Requires CSP v0.2.3-preview47 or later")]
+ public bool EnableCarReset { get; set; } = false;
[YamlMember(Description = "Automatically assign traffic cars based on the car folder name")]
public bool AutoAssignTrafficCars { get; init; } = true;
[YamlMember(Description = "Radius around a player in which AI cars won't despawn")]
@@ -140,4 +143,17 @@ public partial class AiParams : ObservableObject
[YamlIgnore] public float RightLaneOffsetMs => RightLaneOffsetKph / 3.6f;
[YamlIgnore] public int IgnoreObstaclesAfterMilliseconds => IgnoreObstaclesAfterSeconds * 1000;
[YamlIgnore] public int AiBehaviorUpdateIntervalMilliseconds => 1000 / AiBehaviorUpdateIntervalHz;
+
+ internal void ApplyConfigurationFixes(ACServerConfiguration serverConfiguration)
+ {
+ if (AiPerPlayerTargetCount == 0)
+ {
+ AiPerPlayerTargetCount = serverConfiguration.EntryList.Cars.Count(c => c.AiMode != AiMode.None);
+ }
+
+ if (MaxAiTargetCount == 0)
+ {
+ MaxAiTargetCount = serverConfiguration.EntryList.Cars.Count(c => c.AiMode != AiMode.Fixed) * AiPerPlayerTargetCount;
+ }
+ }
}
diff --git a/TrafficAiPlugin/Configuration/TrafficAiConfigurationValidator.cs b/TrafficAiPlugin/Configuration/TrafficAiConfigurationValidator.cs
new file mode 100644
index 00000000..82ad6052
--- /dev/null
+++ b/TrafficAiPlugin/Configuration/TrafficAiConfigurationValidator.cs
@@ -0,0 +1,34 @@
+using FluentValidation;
+using JetBrains.Annotations;
+
+namespace TrafficAiPlugin.Configuration;
+
+// Use FluentValidation to validate plugin configuration
+[UsedImplicitly]
+public class TrafficAiConfigurationValidator : AbstractValidator
+{
+ public TrafficAiConfigurationValidator()
+ {
+ RuleFor(ai => ai.MinSpawnDistancePoints).LessThanOrEqualTo(ai => ai.MaxSpawnDistancePoints);
+ RuleFor(ai => ai.MinAiSafetyDistanceMeters).LessThanOrEqualTo(ai => ai.MaxAiSafetyDistanceMeters);
+ RuleFor(ai => ai.MinSpawnProtectionTimeSeconds).LessThanOrEqualTo(ai => ai.MaxSpawnProtectionTimeSeconds);
+ RuleFor(ai => ai.MinCollisionStopTimeSeconds).LessThanOrEqualTo(ai => ai.MaxCollisionStopTimeSeconds);
+ RuleFor(ai => ai.MaxSpeedVariationPercent).InclusiveBetween(0, 1);
+ RuleFor(ai => ai.DefaultAcceleration).GreaterThan(0);
+ RuleFor(ai => ai.DefaultDeceleration).GreaterThan(0);
+ RuleFor(ai => ai.NamePrefix).NotNull();
+ RuleFor(ai => ai.IgnoreObstaclesAfterSeconds).GreaterThanOrEqualTo(0);
+ RuleFor(ai => ai.HourlyTrafficDensity)
+ .Must(htd => htd?.Count == 24)
+ .When(ai => ai.HourlyTrafficDensity != null)
+ .WithMessage("HourlyTrafficDensity must have exactly 24 entries");
+ RuleFor(ai => ai.CarSpecificOverrides).NotNull();
+ RuleFor(ai => ai.AiBehaviorUpdateIntervalHz).GreaterThan(0);
+ RuleFor(ai => ai.LaneCountSpecificOverrides).NotNull();
+ RuleForEach(ai => ai.LaneCountSpecificOverrides).ChildRules(overrides =>
+ {
+ overrides.RuleFor(o => o.Key).GreaterThan(0);
+ overrides.RuleFor(o => o.Value.MinAiSafetyDistanceMeters).LessThanOrEqualTo(o => o.Value.MaxAiSafetyDistanceMeters);
+ });
+ }
+}
diff --git a/AssettoServer/Server/Ai/Configuration/TrafficConfiguration.cs b/TrafficAiPlugin/Configuration/TrafficConfiguration.cs
similarity index 72%
rename from AssettoServer/Server/Ai/Configuration/TrafficConfiguration.cs
rename to TrafficAiPlugin/Configuration/TrafficConfiguration.cs
index 991f80a5..c5a64789 100644
--- a/AssettoServer/Server/Ai/Configuration/TrafficConfiguration.cs
+++ b/TrafficAiPlugin/Configuration/TrafficConfiguration.cs
@@ -1,7 +1,6 @@
-using System.Collections.Generic;
-using JetBrains.Annotations;
+using JetBrains.Annotations;
-namespace AssettoServer.Server.Ai.Configuration;
+namespace TrafficAiPlugin.Configuration;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
public class TrafficConfiguration
diff --git a/AssettoServer/Server/Ai/DynamicTrafficDensity.cs b/TrafficAiPlugin/DynamicTrafficDensity.cs
similarity index 58%
rename from AssettoServer/Server/Ai/DynamicTrafficDensity.cs
rename to TrafficAiPlugin/DynamicTrafficDensity.cs
index e3329ceb..0556f2db 100644
--- a/AssettoServer/Server/Ai/DynamicTrafficDensity.cs
+++ b/TrafficAiPlugin/DynamicTrafficDensity.cs
@@ -1,22 +1,23 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using AssettoServer.Server.Configuration;
+using AssettoServer.Server.Configuration;
using AssettoServer.Server.Weather;
using AssettoServer.Utils;
using Microsoft.Extensions.Hosting;
using Serilog;
+using TrafficAiPlugin.Configuration;
-namespace AssettoServer.Server.Ai;
+namespace TrafficAiPlugin;
public class DynamicTrafficDensity : BackgroundService
{
- private readonly ACServerConfiguration _configuration;
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly TrafficAiConfiguration _configuration;
private readonly WeatherManager _weatherManager;
- public DynamicTrafficDensity(ACServerConfiguration configuration, WeatherManager weatherManager)
+ public DynamicTrafficDensity(ACServerConfiguration serverConfiguration,
+ TrafficAiConfiguration configuration,
+ WeatherManager weatherManager)
{
+ _serverConfiguration = serverConfiguration;
_configuration = configuration;
_weatherManager = weatherManager;
}
@@ -26,23 +27,25 @@ private float GetDensity(double hourOfDay)
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (Math.Truncate(hourOfDay) == hourOfDay)
{
- return _configuration.Extra.AiParams.HourlyTrafficDensity![(int)hourOfDay];
+ return _configuration.HourlyTrafficDensity![(int)hourOfDay];
}
int lowerBound = (int)Math.Floor(hourOfDay);
int higherBound = (int)Math.Ceiling(hourOfDay) % 24;
- return (float)MathUtils.Lerp(_configuration.Extra.AiParams.HourlyTrafficDensity![lowerBound], _configuration.Extra.AiParams.HourlyTrafficDensity![higherBound], hourOfDay - lowerBound);
+ return (float)MathUtils.Lerp(_configuration.HourlyTrafficDensity![lowerBound], _configuration.HourlyTrafficDensity![higherBound], hourOfDay - lowerBound);
}
public override Task StartAsync(CancellationToken cancellationToken)
{
- if (_configuration.Server.TimeOfDayMultiplier == 0 )
+ if (_configuration.HourlyTrafficDensity == null) return Task.CompletedTask;
+
+ if (_serverConfiguration.Server.TimeOfDayMultiplier == 0 )
{
throw new ConfigurationException("TIME_OF_DAY_MULT in server_cfg.ini must be greater than 0");
}
- foreach (var wfxParam in _configuration.Server.Weathers.Where(w => w.WeatherFxParams.TimeMultiplier.HasValue))
+ foreach (var wfxParam in _serverConfiguration.Server.Weathers.Where(w => w.WeatherFxParams.TimeMultiplier.HasValue))
{
if (wfxParam.WeatherFxParams.TimeMultiplier == 0)
{
@@ -60,7 +63,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
try
{
double hours = _weatherManager.CurrentDateTime.TimeOfDay.TickOfDay / 10_000_000.0 / 3600.0;
- _configuration.Extra.AiParams.TrafficDensity = GetDensity(hours);
+ _configuration.TrafficDensity = GetDensity(hours);
}
catch (Exception ex)
{
@@ -68,7 +71,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
finally
{
- await Task.Delay(TimeSpan.FromMinutes(10.0 / _configuration.Server.TimeOfDayMultiplier), stoppingToken);
+ await Task.Delay(TimeSpan.FromMinutes(10.0 / _serverConfiguration.Server.TimeOfDayMultiplier), stoppingToken);
}
}
}
diff --git a/AssettoServer/Server/EntryCarAi.cs b/TrafficAiPlugin/EntryCarTrafficAi.cs
similarity index 52%
rename from AssettoServer/Server/EntryCarAi.cs
rename to TrafficAiPlugin/EntryCarTrafficAi.cs
index f2f1675c..586217dc 100644
--- a/AssettoServer/Server/EntryCarAi.cs
+++ b/TrafficAiPlugin/EntryCarTrafficAi.cs
@@ -1,33 +1,24 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
+using System.ComponentModel;
using System.Numerics;
using System.Runtime.InteropServices;
-using AssettoServer.Server.Ai;
-using AssettoServer.Server.Ai.Splines;
-using AssettoServer.Server.Configuration.Extra;
+using AssettoServer.Server;
+using AssettoServer.Server.Configuration;
using AssettoServer.Shared.Model;
using AssettoServer.Shared.Network.Packets.Outgoing;
+using AssettoServer.Shared.Network.Packets.Shared;
+using TrafficAiPlugin.Configuration;
+using TrafficAiPlugin.Shared;
+using TrafficAiPlugin.Splines;
-namespace AssettoServer.Server;
+namespace TrafficAiPlugin;
-public enum AiMode
+public class EntryCarTrafficAi : IEntryCarTrafficAi
{
- None,
- Auto,
- Fixed
-}
-
-public partial class EntryCar
-{
- public bool AiControlled { get; set; }
- public AiMode AiMode { get; set; }
public int TargetAiStateCount { get; private set; } = 1;
public byte[] LastSeenAiSpawn { get; }
public byte[] AiPakSequenceIds { get; }
- public AiState?[] LastSeenAiState { get; }
- public string? AiName { get; private set; }
- public bool AiEnableColorChanges { get; set; } = false;
+ public IAiState?[] LastSeenAiState { get; }
+ public bool AiEnableColorChanges => EntryCar.DriverOptionsFlags.HasFlag(DriverOptionsFlags.AllowColorChange);
public int AiIdleEngineRpm { get; set; } = 800;
public int AiMaxEngineRpm { get; set; } = 3000;
public float AiAcceleration { get; set; }
@@ -52,35 +43,68 @@ public partial class EntryCar
private readonly List _aiStates = [];
private Span AiStatesSpan => CollectionsMarshal.AsSpan(_aiStates);
- private readonly Func _aiStateFactory;
- private readonly AiSpline? _spline;
+ private readonly Func _aiStateFactory;
+ private readonly TrafficAi _trafficAi;
+
+ public EntryCar EntryCar { get; }
+
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly TrafficAiConfiguration _configuration;
+ private readonly EntryCarManager _entryCarManager;
+ private readonly SessionManager _sessionManager;
+ private readonly AiSpline _aiSpline;
+
+ public EntryCarTrafficAi(EntryCar entryCar,
+ TrafficAiConfiguration configuration,
+ EntryCarManager entryCarManager,
+ SessionManager sessionManager,
+ ACServerConfiguration serverConfiguration,
+ Func aiStateFactory,
+ TrafficAi trafficAi,
+ AiSpline aiSpline)
+ {
+ EntryCar = entryCar;
+ _configuration = configuration;
+ _entryCarManager = entryCarManager;
+ _sessionManager = sessionManager;
+ _serverConfiguration = serverConfiguration;
+ _aiStateFactory = aiStateFactory;
+ _trafficAi = trafficAi;
+ _aiSpline = aiSpline;
+
+ AiPakSequenceIds = new byte[entryCarManager.EntryCars.Length];
+ LastSeenAiState = new AiState[entryCarManager.EntryCars.Length];
+ LastSeenAiSpawn = new byte[entryCarManager.EntryCars.Length];
+
+ AiInit();
+ }
private void AiInit()
{
- AiName = $"{_configuration.Extra.AiParams.NamePrefix} {SessionId}";
+ EntryCar.AiName = $"{_configuration.NamePrefix} {EntryCar.SessionId}";
SetAiOverbooking(0);
- _configuration.Extra.AiParams.PropertyChanged += OnConfigReload;
+ _configuration.PropertyChanged += OnConfigReload;
OnConfigReload(_configuration, new PropertyChangedEventArgs(string.Empty));
}
private void OnConfigReload(object? sender, PropertyChangedEventArgs args)
{
- AiSplineHeightOffsetMeters = _configuration.Extra.AiParams.SplineHeightOffsetMeters;
- AiAcceleration = _configuration.Extra.AiParams.DefaultAcceleration;
- AiDeceleration = _configuration.Extra.AiParams.DefaultDeceleration;
- AiCorneringSpeedFactor = _configuration.Extra.AiParams.CorneringSpeedFactor;
- AiCorneringBrakeDistanceFactor = _configuration.Extra.AiParams.CorneringBrakeDistanceFactor;
- AiCorneringBrakeForceFactor = _configuration.Extra.AiParams.CorneringBrakeForceFactor;
- TyreDiameterMeters = _configuration.Extra.AiParams.TyreDiameterMeters;
- AiMinSpawnProtectionTimeMilliseconds = _configuration.Extra.AiParams.MinSpawnProtectionTimeMilliseconds;
- AiMaxSpawnProtectionTimeMilliseconds = _configuration.Extra.AiParams.MaxSpawnProtectionTimeMilliseconds;
- AiMinCollisionStopTimeMilliseconds = _configuration.Extra.AiParams.MinCollisionStopTimeMilliseconds;
- AiMaxCollisionStopTimeMilliseconds = _configuration.Extra.AiParams.MaxCollisionStopTimeMilliseconds;
-
- foreach (var carOverrides in _configuration.Extra.AiParams.CarSpecificOverrides)
+ AiSplineHeightOffsetMeters = _configuration.SplineHeightOffsetMeters;
+ AiAcceleration = _configuration.DefaultAcceleration;
+ AiDeceleration = _configuration.DefaultDeceleration;
+ AiCorneringSpeedFactor = _configuration.CorneringSpeedFactor;
+ AiCorneringBrakeDistanceFactor = _configuration.CorneringBrakeDistanceFactor;
+ AiCorneringBrakeForceFactor = _configuration.CorneringBrakeForceFactor;
+ TyreDiameterMeters = _configuration.TyreDiameterMeters;
+ AiMinSpawnProtectionTimeMilliseconds = _configuration.MinSpawnProtectionTimeMilliseconds;
+ AiMaxSpawnProtectionTimeMilliseconds = _configuration.MaxSpawnProtectionTimeMilliseconds;
+ AiMinCollisionStopTimeMilliseconds = _configuration.MinCollisionStopTimeMilliseconds;
+ AiMaxCollisionStopTimeMilliseconds = _configuration.MaxCollisionStopTimeMilliseconds;
+
+ foreach (var carOverrides in _configuration.CarSpecificOverrides)
{
- if (carOverrides.Model == Model)
+ if (carOverrides.Model == EntryCar.Model)
{
if (carOverrides.SplineHeightOffsetMeters.HasValue)
AiSplineHeightOffsetMeters = carOverrides.SplineHeightOffsetMeters.Value;
@@ -134,11 +158,11 @@ public void RemoveUnsafeStates()
{
if (aiState != targetAiState
&& targetAiState.Initialized
- && Vector3.DistanceSquared(aiState.Status.Position, targetAiState.Status.Position) < _configuration.Extra.AiParams.MinStateDistanceSquared
- && (_configuration.Extra.AiParams.TwoWayTraffic || Vector3.Dot(aiState.Status.Velocity, targetAiState.Status.Velocity) > 0))
+ && Vector3.DistanceSquared(aiState.Status.Position, targetAiState.Status.Position) < _configuration.MinStateDistanceSquared
+ && (_configuration.TwoWayTraffic || Vector3.Dot(aiState.Status.Velocity, targetAiState.Status.Velocity) > 0))
{
aiState.Despawn();
- Logger.Verbose("Removed close state from AI {SessionId}", SessionId);
+ EntryCar.Logger.Verbose("Removed close state from AI {SessionId}", EntryCar.SessionId);
}
}
}
@@ -171,7 +195,7 @@ public void AiObstacleDetection()
float distance = Vector3.DistanceSquared(aiState.Status.Position, playerStatus.Position);
- if (_configuration.Extra.AiParams.TwoWayTraffic)
+ if (_configuration.TwoWayTraffic)
{
if (distance < minDistance)
{
@@ -184,8 +208,8 @@ public void AiObstacleDetection()
bool isBestSameDirection = bestState != null && Vector3.Dot(bestState.Status.Velocity, playerStatus.Velocity) > 0;
bool isCandidateSameDirection = Vector3.Dot(aiState.Status.Velocity, playerStatus.Velocity) > 0;
bool isPlayerFastEnough = playerStatus.Velocity.LengthSquared() > 1;
- bool isTieBreaker = minDistance < _configuration.Extra.AiParams.MinStateDistanceSquared &&
- distance < _configuration.Extra.AiParams.MinStateDistanceSquared &&
+ bool isTieBreaker = minDistance < _configuration.MinStateDistanceSquared &&
+ distance < _configuration.MinStateDistanceSquared &&
isPlayerFastEnough;
// Tie breaker: Multiple close states, so take the one with min distance and same direction
@@ -203,9 +227,9 @@ public void AiObstacleDetection()
public bool IsPositionSafe(int pointId)
{
- ArgumentNullException.ThrowIfNull(_spline);
+ ArgumentNullException.ThrowIfNull(_aiSpline);
- var ops = _spline.Operations;
+ var ops = _aiSpline.Operations;
foreach (var aiState in AiStatesSpan)
{
@@ -261,12 +285,12 @@ public bool CanSpawnAiState(Vector3 spawnPoint, AiState aiState)
aiState.Dispose();
_aiStates.Remove(aiState);
- Logger.Verbose("Removed state of Traffic {SessionId} due to overbooking reduction", SessionId);
+ EntryCar.Logger.Verbose("Removed state of Traffic {SessionId} due to overbooking reduction", EntryCar.SessionId);
if (_aiStates.Count == 0)
{
- Logger.Verbose("Traffic {SessionId} has no states left, disconnecting", SessionId);
- _entryCarManager.BroadcastPacket(new CarDisconnected { SessionId = SessionId });
+ EntryCar.Logger.Verbose("Traffic {SessionId} has no states left, disconnecting", EntryCar.SessionId);
+ _entryCarManager.BroadcastPacket(new CarDisconnected { SessionId = EntryCar.SessionId });
}
return false;
@@ -276,7 +300,7 @@ public bool CanSpawnAiState(Vector3 spawnPoint, AiState aiState)
{
if (state == aiState || !state.Initialized) continue;
- if (Vector3.DistanceSquared(spawnPoint, state.Status.Position) < _configuration.Extra.AiParams.StateSpawnDistanceSquared)
+ if (Vector3.DistanceSquared(spawnPoint, state.Status.Position) < _configuration.StateSpawnDistanceSquared)
{
return false;
}
@@ -287,48 +311,46 @@ public bool CanSpawnAiState(Vector3 spawnPoint, AiState aiState)
public void SetAiControl(bool aiControlled)
{
- if (AiControlled != aiControlled)
+ if (EntryCar.AiControlled == aiControlled) return;
+
+ EntryCar.AiControlled = aiControlled;
+ if (EntryCar.AiControlled)
{
- AiControlled = aiControlled;
+ EntryCar.Logger.Debug("Slot {SessionId} is now controlled by AI", EntryCar.SessionId);
- if (AiControlled)
+ AiReset();
+ _entryCarManager.BroadcastPacket(new CarConnected
{
- Logger.Debug("Slot {SessionId} is now controlled by AI", SessionId);
-
- AiReset();
- _entryCarManager.BroadcastPacket(new CarConnected
+ SessionId = EntryCar.SessionId,
+ Name = EntryCar.AiName
+ });
+ if (_configuration.HideAiCars)
+ {
+ _entryCarManager.BroadcastPacket(new CSPCarVisibilityUpdate
{
- SessionId = SessionId,
- Name = AiName
+ SessionId = EntryCar.SessionId,
+ Visible = CSPCarVisibility.Invisible
});
- if (_configuration.Extra.AiParams.HideAiCars)
- {
- _entryCarManager.BroadcastPacket(new CSPCarVisibilityUpdate
- {
- SessionId = SessionId,
- Visible = CSPCarVisibility.Invisible
- });
- }
}
- else
+ }
+ else
+ {
+ EntryCar.Logger.Debug("Slot {SessionId} is no longer controlled by AI", EntryCar.SessionId);
+ if (_aiStates.Count > 0)
{
- Logger.Debug("Slot {SessionId} is no longer controlled by AI", SessionId);
- if (_aiStates.Count > 0)
- {
- _entryCarManager.BroadcastPacket(new CarDisconnected { SessionId = SessionId });
- }
+ _entryCarManager.BroadcastPacket(new CarDisconnected { SessionId = EntryCar.SessionId });
+ }
- if (_configuration.Extra.AiParams.HideAiCars)
+ if (_configuration.HideAiCars)
+ {
+ _entryCarManager.BroadcastPacket(new CSPCarVisibilityUpdate
{
- _entryCarManager.BroadcastPacket(new CSPCarVisibilityUpdate
- {
- SessionId = SessionId,
- Visible = CSPCarVisibility.Visible
- });
- }
-
- AiReset();
+ SessionId = EntryCar.SessionId,
+ Visible = CSPCarVisibility.Visible
+ });
}
+
+ AiReset();
}
}
@@ -360,4 +382,123 @@ private void AiReset()
_aiStates.Clear();
_aiStates.Add(_aiStateFactory(this));
}
+
+ public bool TryResetPosition()
+ {
+ if (_sessionManager.ServerTimeMilliseconds < _sessionManager.CurrentSession.StartTimeMilliseconds + 20_000
+ || (_sessionManager.ServerTimeMilliseconds > _sessionManager.CurrentSession.EndTimeMilliseconds
+ && _sessionManager.CurrentSession.EndTimeMilliseconds > 0))
+ return false;
+
+ EntryCar.SetCollisions(false);
+
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(250);
+
+ var (splinePointId, _) = _aiSpline.WorldToSpline(EntryCar.Status.Position);
+
+ var splinePoint = _aiSpline.Points[splinePointId];
+
+ var position = splinePoint.Position;
+ var direction = - _aiSpline.Operations.GetForwardVector(splinePoint.NextId);
+
+ EntryCar.Client?.SendTeleportCarPacket(position, direction);
+ await Task.Delay(10000);
+ EntryCar.SetCollisions(true);
+ });
+
+ EntryCar.Logger.Information("Reset position for {Player} ({SessionId})",EntryCar.Client?.Name, EntryCar.Client?.SessionId);
+ return true;
+ }
+
+ public bool GetPositionUpdateForCar(EntryCar toCar, out PositionUpdateOut positionUpdateOut)
+ {
+ CarStatus targetCarStatus;
+ var toTargetCar = toCar.TargetCar;
+ if (toTargetCar != null)
+ {
+ var toTargetCarAi = _trafficAi.GetAiCarBySessionId(toTargetCar.SessionId);
+ if (toTargetCar.AiControlled && toTargetCarAi.LastSeenAiState[toCar.SessionId] != null)
+ {
+ targetCarStatus = toTargetCarAi.LastSeenAiState[toCar.SessionId]!.Status;
+ }
+ else
+ {
+ targetCarStatus = toTargetCar.Status;
+ }
+ }
+ else
+ {
+ targetCarStatus = toCar.Status;
+ }
+
+ CarStatus status;
+ if (EntryCar.AiControlled)
+ {
+ var aiState = GetBestStateForPlayer(targetCarStatus);
+
+ if (aiState == null)
+ {
+ positionUpdateOut = default;
+ return false;
+ }
+
+ if (LastSeenAiState[toCar.SessionId] != aiState
+ || LastSeenAiSpawn[toCar.SessionId] != aiState.SpawnCounter)
+ {
+ LastSeenAiState[toCar.SessionId] = aiState;
+ LastSeenAiSpawn[toCar.SessionId] = aiState.SpawnCounter;
+
+ if (AiEnableColorChanges)
+ {
+ toCar.Client?.SendPacket(new CSPCarColorUpdate
+ {
+ SessionId = EntryCar.SessionId,
+ Color = aiState.Color
+ });
+ }
+ }
+
+ status = aiState.Status;
+ }
+ else
+ {
+ status = EntryCar.Status;
+ }
+
+ float distanceSquared = Vector3.DistanceSquared(status.Position, targetCarStatus.Position);
+ if (EntryCar.TargetCar != null || distanceSquared > EntryCar.NetworkDistanceSquared)
+ {
+ if ((_sessionManager.ServerTimeMilliseconds - EntryCar.OtherCarsLastSentUpdateTime[toCar.SessionId]) < EntryCar.OutsideNetworkBubbleUpdateRateMs)
+ {
+ positionUpdateOut = default;
+ return false;
+ }
+
+ EntryCar.OtherCarsLastSentUpdateTime[toCar.SessionId] = _sessionManager.ServerTimeMilliseconds;
+ }
+
+ positionUpdateOut = new PositionUpdateOut(EntryCar.SessionId,
+ EntryCar.AiControlled ? AiPakSequenceIds[toCar.SessionId]++ : status.PakSequenceId,
+ (uint)(status.Timestamp - toCar.TimeOffset),
+ EntryCar.Ping,
+ status.Position,
+ status.Rotation,
+ status.Velocity,
+ status.TyreAngularSpeed[0],
+ status.TyreAngularSpeed[1],
+ status.TyreAngularSpeed[2],
+ status.TyreAngularSpeed[3],
+ status.SteerAngle,
+ status.WheelAngle,
+ status.EngineRpm,
+ status.Gear,
+ (_serverConfiguration.Extra.ForceLights || EntryCar.ForceLights)
+ ? status.StatusFlag | CarStatusFlags.LightsOn
+ : status.StatusFlag,
+ status.PerformanceDelta,
+ status.Gas);
+ return true;
+ }
}
diff --git a/AssettoServer/Network/ClientMessages/RequestResetPacket.cs b/TrafficAiPlugin/Packets/RequestResetPacket.cs
similarity index 68%
rename from AssettoServer/Network/ClientMessages/RequestResetPacket.cs
rename to TrafficAiPlugin/Packets/RequestResetPacket.cs
index a62b0b16..85993e5e 100644
--- a/AssettoServer/Network/ClientMessages/RequestResetPacket.cs
+++ b/TrafficAiPlugin/Packets/RequestResetPacket.cs
@@ -1,4 +1,6 @@
-namespace AssettoServer.Network.ClientMessages;
+using AssettoServer.Network.ClientMessages;
+
+namespace TrafficAiPlugin.Packets;
[OnlineEvent(Key = "AS_RequestResetCar")]
public class RequestResetPacket : OnlineEvent
diff --git a/AssettoServer/Server/Ai/Splines/AdjacentLaneDetector.cs b/TrafficAiPlugin/Splines/AdjacentLaneDetector.cs
similarity index 96%
rename from AssettoServer/Server/Ai/Splines/AdjacentLaneDetector.cs
rename to TrafficAiPlugin/Splines/AdjacentLaneDetector.cs
index f0ec89d1..f2b52bf3 100644
--- a/AssettoServer/Server/Ai/Splines/AdjacentLaneDetector.cs
+++ b/TrafficAiPlugin/Splines/AdjacentLaneDetector.cs
@@ -1,9 +1,9 @@
-using System;
-using System.Numerics;
+using System.Numerics;
using Serilog;
using SerilogTimings;
+using TrafficAiPlugin.Shared.Splines;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public static class AdjacentLaneDetector
{
diff --git a/AssettoServer/Server/Ai/Splines/AiSpline.cs b/TrafficAiPlugin/Splines/AiSpline.cs
similarity index 93%
rename from AssettoServer/Server/Ai/Splines/AiSpline.cs
rename to TrafficAiPlugin/Splines/AiSpline.cs
index 6f46de19..ff44a72d 100644
--- a/AssettoServer/Server/Ai/Splines/AiSpline.cs
+++ b/TrafficAiPlugin/Splines/AiSpline.cs
@@ -1,6 +1,4 @@
-using System;
-using System.Buffers;
-using System.IO;
+using System.Buffers;
using System.IO.MemoryMappedFiles;
using System.Numerics;
using System.Runtime.InteropServices;
@@ -9,10 +7,12 @@
using DotNext.Runtime.InteropServices;
using Serilog;
using Supercluster.KDTree;
+using TrafficAiPlugin.Shared;
+using TrafficAiPlugin.Shared.Splines;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
-public class AiSpline : IDisposable
+public class AiSpline : IAiSpline, IDisposable
{
public const int SupportedVersion = 1;
@@ -102,4 +102,7 @@ public void Dispose()
_fileAccessor.Dispose();
_file.Dispose();
}
+
+ public Vector3 GetForwardVector(int pointId)
+ => Operations.GetForwardVector(pointId);
}
diff --git a/AssettoServer/Server/Ai/Splines/AiSplineHeader.cs b/TrafficAiPlugin/Splines/AiSplineHeader.cs
similarity index 75%
rename from AssettoServer/Server/Ai/Splines/AiSplineHeader.cs
rename to TrafficAiPlugin/Splines/AiSplineHeader.cs
index 2e646502..b650084c 100644
--- a/AssettoServer/Server/Ai/Splines/AiSplineHeader.cs
+++ b/TrafficAiPlugin/Splines/AiSplineHeader.cs
@@ -1,4 +1,4 @@
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public struct AiSplineHeader
{
diff --git a/AssettoServer/Server/Ai/Splines/AiSplineLocator.cs b/TrafficAiPlugin/Splines/AiSplineLocator.cs
similarity index 96%
rename from AssettoServer/Server/Ai/Splines/AiSplineLocator.cs
rename to TrafficAiPlugin/Splines/AiSplineLocator.cs
index 37a3ad11..c7fdd6e6 100644
--- a/AssettoServer/Server/Ai/Splines/AiSplineLocator.cs
+++ b/TrafficAiPlugin/Splines/AiSplineLocator.cs
@@ -1,12 +1,9 @@
-using System;
-using System.IO;
-using System.IO.Hashing;
-using System.Linq;
+using System.IO.Hashing;
using AssettoServer.Server.Configuration;
using Serilog;
using SerilogTimings;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class AiSplineLocator
{
diff --git a/AssettoServer/Server/Ai/Splines/AiSplineWriter.cs b/TrafficAiPlugin/Splines/AiSplineWriter.cs
similarity index 93%
rename from AssettoServer/Server/Ai/Splines/AiSplineWriter.cs
rename to TrafficAiPlugin/Splines/AiSplineWriter.cs
index ee1c6a70..d9f412f5 100644
--- a/AssettoServer/Server/Ai/Splines/AiSplineWriter.cs
+++ b/TrafficAiPlugin/Splines/AiSplineWriter.cs
@@ -1,8 +1,7 @@
-using System.IO;
-using AssettoServer.Utils;
+using AssettoServer.Utils;
using Serilog;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class AiSplineWriter
{
diff --git a/AssettoServer/Server/Ai/Splines/FastLane.cs b/TrafficAiPlugin/Splines/FastLane.cs
similarity index 65%
rename from AssettoServer/Server/Ai/Splines/FastLane.cs
rename to TrafficAiPlugin/Splines/FastLane.cs
index a58d191e..d2aeb0ec 100644
--- a/AssettoServer/Server/Ai/Splines/FastLane.cs
+++ b/TrafficAiPlugin/Splines/FastLane.cs
@@ -1,6 +1,6 @@
-using System;
+using TrafficAiPlugin.Shared.Splines;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class FastLane
{
diff --git a/AssettoServer/Server/Ai/Splines/FastLaneParser.cs b/TrafficAiPlugin/Splines/FastLaneParser.cs
similarity index 92%
rename from AssettoServer/Server/Ai/Splines/FastLaneParser.cs
rename to TrafficAiPlugin/Splines/FastLaneParser.cs
index 18ba8e4a..51a3c245 100644
--- a/AssettoServer/Server/Ai/Splines/FastLaneParser.cs
+++ b/TrafficAiPlugin/Splines/FastLaneParser.cs
@@ -1,25 +1,24 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
+using System.IO.Compression;
using System.Numerics;
-using AssettoServer.Server.Ai.Configuration;
using AssettoServer.Server.Configuration;
using AssettoServer.Utils;
using Serilog;
+using TrafficAiPlugin.Configuration;
+using TrafficAiPlugin.Shared.Splines;
using YamlDotNet.Serialization;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class FastLaneParser
{
- private readonly ACServerConfiguration _configuration;
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly TrafficAiConfiguration _configuration;
private ILogger _logger = Log.Logger;
- public FastLaneParser(ACServerConfiguration configuration)
+ public FastLaneParser(ACServerConfiguration serverConfiguration, TrafficAiConfiguration configuration)
{
+ _serverConfiguration = serverConfiguration;
_configuration = configuration;
}
@@ -30,7 +29,7 @@ private void CheckConfig(TrafficConfiguration configuration)
Log.Information("Loading AI spline by {Author}, version {Version}", configuration.Author, configuration.Version);
}
- if (!string.IsNullOrWhiteSpace(configuration.Track) && Path.GetFileName(_configuration.Server.Track) != configuration.Track)
+ if (!string.IsNullOrWhiteSpace(configuration.Track) && Path.GetFileName(_serverConfiguration.Server.Track) != configuration.Track)
{
throw new InvalidOperationException($"Mismatched AI spline, AI spline is for track {configuration.Track}");
}
@@ -114,7 +113,7 @@ public MutableAiSpline FromFiles(string folder)
throw new InvalidOperationException($"No AI splines found. Please put at least one AI spline fast_lane.ai(p) into {Path.GetFullPath(folder)}");
}
- return new MutableAiSpline(splines, _configuration.Extra.AiParams.LaneWidthMeters, _configuration.Extra.AiParams.TwoWayTraffic, configuration, _logger);
+ return new MutableAiSpline(splines, _configuration.LaneWidthMeters, _configuration.TwoWayTraffic, configuration, _logger);
}
private SplinePoint[] FromFileV7(BinaryReader reader, int idOffset)
@@ -213,7 +212,7 @@ private FastLane FromFile(Stream file, string name, int idOffset = 0)
};
MovingAverage? avg = null;
- if (_configuration.Extra.AiParams.SmoothCamber)
+ if (_configuration.SmoothCamber)
{
avg = new MovingAverage(5);
}
diff --git a/AssettoServer/Server/Ai/Splines/JunctionEvaluator.cs b/TrafficAiPlugin/Splines/JunctionEvaluator.cs
similarity index 96%
rename from AssettoServer/Server/Ai/Splines/JunctionEvaluator.cs
rename to TrafficAiPlugin/Splines/JunctionEvaluator.cs
index b55162df..12b52269 100644
--- a/AssettoServer/Server/Ai/Splines/JunctionEvaluator.cs
+++ b/TrafficAiPlugin/Splines/JunctionEvaluator.cs
@@ -1,7 +1,6 @@
-using System;
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class JunctionEvaluator
{
diff --git a/AssettoServer/Server/Ai/Splines/MutableAiSpline.cs b/TrafficAiPlugin/Splines/MutableAiSpline.cs
similarity index 96%
rename from AssettoServer/Server/Ai/Splines/MutableAiSpline.cs
rename to TrafficAiPlugin/Splines/MutableAiSpline.cs
index fab45127..2b52827e 100644
--- a/AssettoServer/Server/Ai/Splines/MutableAiSpline.cs
+++ b/TrafficAiPlugin/Splines/MutableAiSpline.cs
@@ -1,13 +1,11 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-using AssettoServer.Server.Ai.Configuration;
+using System.Numerics;
using AssettoServer.Shared.Network.Packets.Outgoing;
using Serilog;
using Supercluster.KDTree;
+using TrafficAiPlugin.Configuration;
+using TrafficAiPlugin.Shared.Splines;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class MutableAiSpline
{
diff --git a/AssettoServer/Server/Ai/Splines/SlowestAiStates.cs b/TrafficAiPlugin/Splines/SlowestAiStates.cs
similarity index 96%
rename from AssettoServer/Server/Ai/Splines/SlowestAiStates.cs
rename to TrafficAiPlugin/Splines/SlowestAiStates.cs
index d9a4d7df..059c50cc 100644
--- a/AssettoServer/Server/Ai/Splines/SlowestAiStates.cs
+++ b/TrafficAiPlugin/Splines/SlowestAiStates.cs
@@ -1,6 +1,4 @@
-using System.Threading;
-
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
public class SlowestAiStates
{
diff --git a/AssettoServer/Server/Ai/Splines/SplineJunction.cs b/TrafficAiPlugin/Splines/SplineJunction.cs
similarity index 91%
rename from AssettoServer/Server/Ai/Splines/SplineJunction.cs
rename to TrafficAiPlugin/Splines/SplineJunction.cs
index aa6c239c..fd48cbb6 100644
--- a/AssettoServer/Server/Ai/Splines/SplineJunction.cs
+++ b/TrafficAiPlugin/Splines/SplineJunction.cs
@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using AssettoServer.Shared.Network.Packets.Outgoing;
-namespace AssettoServer.Server.Ai.Splines;
+namespace TrafficAiPlugin.Splines;
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SplineJunction
diff --git a/TrafficAiPlugin/TrafficAi.cs b/TrafficAiPlugin/TrafficAi.cs
new file mode 100644
index 00000000..f11d1e39
--- /dev/null
+++ b/TrafficAiPlugin/TrafficAi.cs
@@ -0,0 +1,109 @@
+using AssettoServer.Network.Tcp;
+using AssettoServer.Server;
+using AssettoServer.Server.Configuration;
+using AssettoServer.Shared.Network.Packets.Outgoing;
+using AssettoServer.Utils;
+using Microsoft.Extensions.Hosting;
+using TrafficAiPlugin.Packets;
+using TrafficAiPlugin.Shared;
+
+namespace TrafficAiPlugin;
+
+using TrafficAiConfiguration = Configuration.TrafficAiConfiguration;
+
+public class TrafficAi : BackgroundService, ITrafficAi
+{
+ private readonly TrafficAiConfiguration _configuration;
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly EntryCarManager _entryCarManager;
+ private readonly SessionManager _sessionManager;
+ private readonly Func _entryCarTrafficAiFactory;
+
+ public readonly List Instances = [];
+
+ public TrafficAi(TrafficAiConfiguration configuration,
+ ACServerConfiguration serverConfiguration,
+ EntryCarManager entryCarManager,
+ SessionManager sessionManager,
+ Func entryCarTrafficAiFactory,
+ CSPClientMessageTypeManager cspClientMessageTypeManager)
+ {
+ _configuration = configuration;
+ _serverConfiguration = serverConfiguration;
+ _entryCarManager = entryCarManager;
+ _sessionManager = sessionManager;
+ _entryCarTrafficAiFactory = entryCarTrafficAiFactory;
+
+ _configuration.ApplyConfigurationFixes(_serverConfiguration);
+
+ if (_configuration.EnableCarReset)
+ {
+ if (!_serverConfiguration.Extra.EnableClientMessages || _serverConfiguration.CSPTrackOptions.MinimumCSPVersion < CSPVersion.V0_2_8)
+ {
+ throw new ConfigurationException(
+ "Reset car: Minimum required CSP version of v0.2.8 (3424); Requires enabled client messages; Requires working AI spline");
+ }
+ cspClientMessageTypeManager.RegisterOnlineEvent((client, _) => { OnResetCar(client); });
+ }
+
+ _entryCarManager.ClientConnected += (sender, _) =>
+ {
+ if (_configuration.HideAiCars)
+ {
+ sender.FirstUpdateSent += OnFirstUpdateSentHideCars;
+ }
+ sender.HandshakeAccepted += OnHandshakeAccepted;
+ };
+ }
+
+ private void OnHandshakeAccepted(ACTcpClient sender, HandshakeAcceptedEventArgs args)
+ {
+ // Gracefully despawn AI cars
+ GetAiCarBySessionId(sender.SessionId).SetAiOverbooking(0);
+ }
+
+ private void OnFirstUpdateSentHideCars(ACTcpClient sender, EventArgs args)
+ {
+ sender.SendPacket(new CSPCarVisibilityUpdate
+ {
+ SessionId = sender.SessionId,
+ Visible = sender.EntryCar.AiControlled ? CSPCarVisibility.Invisible : CSPCarVisibility.Visible
+ });
+ }
+
+ private void OnResetCar(ACTcpClient sender)
+ {
+ if (!_configuration.EnableCarReset) return;
+ GetAiCarBySessionId(sender.SessionId).TryResetPosition();
+ }
+
+ // public EntryCarTrafficAi GetAiCarBySessionId(byte sessionId)
+ // => Instances.First(x => x.EntryCar.SessionId == sessionId);
+
+ IEntryCarTrafficAi ITrafficAi.GetAiCarBySessionId(byte sessionId)
+ => GetAiCarBySessionId(sessionId);
+
+ internal EntryCarTrafficAi GetAiCarBySessionId(byte sessionId)
+ => Instances.First(x => x.EntryCar.SessionId == sessionId);
+
+ public float GetLaneWidthMeters()
+ => _configuration.LaneWidthMeters;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ foreach (var car in _entryCarManager.EntryCars)
+ {
+ var entry = _serverConfiguration.EntryList.Cars[car.SessionId];
+
+ if (_configuration.AutoAssignTrafficCars && entry.Model.Contains("traffic"))
+ {
+ entry.AiMode = AiMode.Fixed;
+ }
+
+ car.AiMode = entry.AiMode;
+ car.AiControlled = entry.AiMode != AiMode.None;
+
+ Instances.Add(_entryCarTrafficAiFactory(car));
+ }
+ }
+}
diff --git a/TrafficAiPlugin/TrafficAiCommandModule.cs b/TrafficAiPlugin/TrafficAiCommandModule.cs
new file mode 100644
index 00000000..fa6aa7b7
--- /dev/null
+++ b/TrafficAiPlugin/TrafficAiCommandModule.cs
@@ -0,0 +1,55 @@
+using AssettoServer.Commands;
+using AssettoServer.Commands.Attributes;
+using AssettoServer.Server;
+using AssettoServer.Server.Configuration;
+using AssettoServer.Utils;
+using JetBrains.Annotations;
+using Qmmands;
+using TrafficAiPlugin.Configuration;
+
+namespace TrafficAiPlugin;
+
+[RequireAdmin]
+[UsedImplicitly(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.WithMembers)]
+public class TrafficAiCommandModule : ACModuleBase
+{
+ private readonly ACServerConfiguration _serverConfiguration;
+ private readonly TrafficAiConfiguration _configuration;
+ private readonly EntryCarManager _entryCarManager;
+ private readonly TrafficAi _trafficAi;
+
+ public TrafficAiCommandModule(ACServerConfiguration serverConfiguration,
+ TrafficAiConfiguration configuration,
+ EntryCarManager entryCarManager,
+ TrafficAi trafficAi)
+ {
+ _serverConfiguration = serverConfiguration;
+ _configuration = configuration;
+ _entryCarManager = entryCarManager;
+ _trafficAi = trafficAi;
+ }
+
+ [Command("setaioverbooking")]
+ public void SetAiOverbooking(int count)
+ {
+ foreach (var aiCar in _trafficAi.Instances.Where(car => car.EntryCar is { AiControlled: true, Client: null }))
+ {
+ aiCar.SetAiOverbooking(count);
+ }
+ Reply($"AI overbooking set to {count}");
+ }
+
+ [Command("resetcar"), RequireConnectedPlayer]
+ public void ResetCarAsync()
+ {
+ if (_serverConfiguration.Extra is { EnableClientMessages: true, MinimumCSPVersion: >= CSPVersion.V0_2_8 } &&
+ _configuration.EnableCarReset)
+ {
+ Reply(_trafficAi.GetAiCarBySessionId(Client!.SessionId).TryResetPosition()
+ ? "Position successfully reset"
+ : "Couldn't reset position");
+ }
+ else
+ Reply("Reset is not enabled on this server");
+ }
+}
diff --git a/TrafficAiPlugin/TrafficAiModule.cs b/TrafficAiPlugin/TrafficAiModule.cs
new file mode 100644
index 00000000..ee036a5e
--- /dev/null
+++ b/TrafficAiPlugin/TrafficAiModule.cs
@@ -0,0 +1,92 @@
+using System.Numerics;
+using AssettoServer.Server.OpenSlotFilters;
+using AssettoServer.Server.Plugin;
+using Autofac;
+using Microsoft.Extensions.Hosting;
+using TrafficAiPlugin.Configuration;
+using TrafficAiPlugin.Shared;
+using TrafficAiPlugin.Splines;
+
+namespace TrafficAiPlugin;
+
+public class TrafficAiModule : AssettoServerModule
+{
+
+ protected override void Load(ContainerBuilder builder)
+ {
+ builder.RegisterType().AsSelf().As().As().SingleInstance();
+ builder.RegisterType().AsSelf().As();
+
+ builder.RegisterType().AsSelf().As();
+
+ builder.RegisterType().AsSelf().As().SingleInstance();
+ builder.RegisterType().AsSelf().SingleInstance().AutoActivate();
+ builder.RegisterType().As();
+
+ builder.RegisterType().As().SingleInstance();
+
+ builder.RegisterType().AsSelf();
+ builder.RegisterType().AsSelf();
+ builder.RegisterType().AsSelf();
+ builder.Register((AiSplineLocator locator) => locator.Locate()).AsSelf().As().SingleInstance();
+ }
+
+ public override object? ReferenceConfiguration => new TrafficAiConfiguration
+ {
+ CarSpecificOverrides =
+ [
+ new CarSpecificOverrides
+ {
+ Model = "my_car_model",
+ Acceleration = 2.5f,
+ Deceleration = 8.5f,
+ AllowedLanes = [LaneSpawnBehavior.Left, LaneSpawnBehavior.Middle, LaneSpawnBehavior.Right],
+ MaxOverbooking = 1,
+ CorneringSpeedFactor = 0.5f,
+ CorneringBrakeDistanceFactor = 3,
+ CorneringBrakeForceFactor = 0.5f,
+ EngineIdleRpm = 800,
+ EngineMaxRpm = 3000,
+ MaxLaneCount = 2,
+ MinLaneCount = 1,
+ TyreDiameterMeters = 0.8f,
+ SplineHeightOffsetMeters = 0,
+ VehicleLengthPostMeters = 2,
+ VehicleLengthPreMeters = 2,
+ MinAiSafetyDistanceMeters = 20,
+ MaxAiSafetyDistanceMeters = 25,
+ MinCollisionStopTimeSeconds = 0,
+ MaxCollisionStopTimeSeconds = 0,
+ MinSpawnProtectionTimeSeconds = 30,
+ MaxSpawnProtectionTimeSeconds = 60
+ }
+ ],
+ LaneCountSpecificOverrides = new Dictionary
+ {
+ {
+ 1,
+ new LaneCountSpecificOverrides
+ {
+ MinAiSafetyDistanceMeters = 50,
+ MaxAiSafetyDistanceMeters = 100
+ }
+ },
+ {
+ 2,
+ new LaneCountSpecificOverrides
+ {
+ MinAiSafetyDistanceMeters = 40,
+ MaxAiSafetyDistanceMeters = 80
+ }
+ }
+ },
+ IgnorePlayerObstacleSpheres =
+ [
+ new Sphere
+ {
+ Center = new Vector3(0, 0, 0),
+ RadiusMeters = 50
+ }
+ ]
+ };
+}
diff --git a/TrafficAiPlugin/TrafficAiPlugin.csproj b/TrafficAiPlugin/TrafficAiPlugin.csproj
new file mode 100644
index 00000000..93c3bcf0
--- /dev/null
+++ b/TrafficAiPlugin/TrafficAiPlugin.csproj
@@ -0,0 +1,49 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+ false
+ embedded
+ ..\out-$(RuntimeIdentifier)\plugins\$(MSBuildProjectName)\
+ $(MSBuildProjectDirectory)=$(MSBuildProjectName)
+
+
+
+ false
+ ..\AssettoServer\bin\$(Configuration)\$(TargetFramework)\plugins\$(MSBuildProjectName)
+
+
+
+
+ false
+ runtime
+
+
+ false
+ runtime
+
+
+ all
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/TrafficAiPlugin/TrafficAiUpdater.cs b/TrafficAiPlugin/TrafficAiUpdater.cs
new file mode 100644
index 00000000..25d55eb3
--- /dev/null
+++ b/TrafficAiPlugin/TrafficAiUpdater.cs
@@ -0,0 +1,92 @@
+using AssettoServer.Server;
+using AssettoServer.Shared.Network.Packets.Outgoing;
+using Serilog;
+
+namespace TrafficAiPlugin;
+
+public class TrafficAiUpdater
+{
+ private readonly EntryCarManager _entryCarManager;
+ private readonly SessionManager _sessionManager;
+ private readonly TrafficAi _trafficAi;
+
+ public TrafficAiUpdater(
+ EntryCarManager entryCarManager,
+ SessionManager sessionManager,
+ ACServer server,
+ TrafficAi trafficAi)
+ {
+ _entryCarManager = entryCarManager;
+ _sessionManager = sessionManager;
+ _trafficAi = trafficAi;
+
+ server.Update += OnUpdate;
+ }
+
+ private void OnUpdate(object sender, EventArgs args)
+ {
+ try
+ {
+ foreach (var instance in _trafficAi.Instances)
+ {
+ instance.AiUpdate();
+ }
+
+ Dictionary> positionUpdates = new();
+ foreach (var entryCar in _entryCarManager.EntryCars)
+ {
+ positionUpdates[entryCar] = [];
+ }
+
+ foreach (var fromCar in _trafficAi.Instances)
+ {
+ if (!fromCar.EntryCar.AiControlled) continue;
+
+ foreach (var (_, toCar) in _entryCarManager.ConnectedCars)
+ {
+ var toClient = toCar.Client;
+ if (toCar == fromCar.EntryCar
+ || toClient == null || !toClient.HasSentFirstUpdate || !toClient.HasUdpEndpoint
+ || !fromCar.GetPositionUpdateForCar(toCar, out var update)) continue;
+
+ if (toClient.SupportsCSPCustomUpdate)
+ {
+ positionUpdates[toCar].Add(update);
+ }
+ else
+ {
+ toClient.SendPacketUdp(in update);
+ }
+ }
+ }
+
+ foreach (var (toCar, updates) in positionUpdates)
+ {
+ if (updates.Count == 0) continue;
+
+ var toClient = toCar.Client;
+ if (toClient == null) continue;
+
+ const int chunkSize = 20;
+ for (int i = 0; i < updates.Count; i += chunkSize)
+ {
+ if (toClient.SupportsCSPCustomUpdate)
+ {
+ var packet = new CSPPositionUpdate(new ArraySegment(updates.ToArray(), i, Math.Min(chunkSize, updates.Count - i)));
+ toClient.SendPacketUdp(in packet);
+ }
+ else
+ {
+ var packet = new BatchedPositionUpdate((uint)(_sessionManager.ServerTimeMilliseconds - toCar.TimeOffset), toCar.Ping,
+ new ArraySegment(updates.ToArray(), i, Math.Min(chunkSize, updates.Count - i)));
+ toClient.SendPacketUdp(in packet);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during ghost car update");
+ }
+ }
+}
diff --git a/TrafficAiPlugin/configuration.json b/TrafficAiPlugin/configuration.json
new file mode 100644
index 00000000..60c483d8
--- /dev/null
+++ b/TrafficAiPlugin/configuration.json
@@ -0,0 +1,5 @@
+{
+ "ExportedAssemblies": [
+ "TrafficAiPlugin.Shared.dll"
+ ]
+}
diff --git a/AssettoServer/Server/Ai/ai_debug.lua b/TrafficAiPlugin/lua/ai_debug.lua
similarity index 100%
rename from AssettoServer/Server/Ai/ai_debug.lua
rename to TrafficAiPlugin/lua/ai_debug.lua
diff --git a/TrafficAiPlugin/lua/resetcar.lua b/TrafficAiPlugin/lua/resetcar.lua
new file mode 100644
index 00000000..a3aaed4b
--- /dev/null
+++ b/TrafficAiPlugin/lua/resetcar.lua
@@ -0,0 +1,10 @@
+local requestResetCarEvent = ac.OnlineEvent({
+ ac.StructItem.key("AS_RequestResetCar"),
+ dummy = ac.StructItem.byte(),
+}, function (sender, message)
+ if sender ~= nil then return end
+ ac.debug("request_reset_car", message.dummy)
+end)
+
+local resetCarControl = ac.ControlButton('__EXT_CMD_RESET', nil)
+resetCarControl:onPressed(function() requestResetCarEvent({}) end)