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)