From f25152badb20364ac73553f50d48cd75843c9231 Mon Sep 17 00:00:00 2001 From: T3M8029 <102697658+T3M8029@users.noreply.github.com> Date: Sun, 28 Jun 2026 01:35:39 +0200 Subject: [PATCH 1/4] mostly working --- .../Networking/Jobs/NetworkedJob.cs | 20 ++ .../Networking/Jobs/NetworkedTask.cs | 29 ++- .../Networking/Train/NetworkedCarSpawner.cs | 2 +- .../Networking/Train/NetworkedTrainCar.cs | 8 +- .../World/NetworkedStationController.cs | 69 +++++-- Multiplayer/Multiplayer.cs | 47 +++++ .../Managers/Client/NetworkClient.cs | 176 ++++++++++++++++-- .../Networking/Managers/NetworkManager.cs | 2 + .../Managers/Server/NetworkServer.cs | 108 ++++++++++- .../Jobs/ClientboundTaskUpdatePacket.cs | 6 + .../Train/ClientboudCouplingStatePacket.cs | 12 ++ .../Jobs/ServerboundJobsRequestPacket.cs | 10 + .../StationProceduralJobsControllerPatch.cs | 20 +- Multiplayer/Patches/Train/CarSpawnerPatch.cs | 18 ++ .../Patches/World/CarsSaveManagerPatch.cs | 59 +++++- 15 files changed, 544 insertions(+), 42 deletions(-) create mode 100644 Multiplayer/Networking/Packets/Clientbound/Train/ClientboudCouplingStatePacket.cs create mode 100644 Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 0aefe94c..56f4828f 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -1,10 +1,13 @@ using DV.CabControls; using DV.InventorySystem; using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.API; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data.Jobs; using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Multiplayer.Components.Networking.Jobs; @@ -173,6 +176,12 @@ public void Initialize(Job job, NetworkedStationController station) job.JobCompleted += OnJobCompleted; job.JobExpired += OnJobExpired; + if (Multiplayer.PersJobs) + { + Multiplayer.PersJobsJobTrackChangedEventRegMethod.Invoke(null, [(Action)OnJobTrackChanged]); + Multiplayer.PersJobsJobCarChangedEventRegMethod.Invoke(null, [(Action<(Job, Car)>)OnJobCarChanged]); + } + // If this is called after Start(), we need to add to cache here if (gameObject.activeInHierarchy) { @@ -277,6 +286,17 @@ private void OnJobExpired(Job job) OnJobDirty?.Invoke(this); } + private void OnJobTrackChanged(Job job) + { + if (job.ID == Job.ID) foreach (var task in job.tasks) NetworkedTask.DoOnActualTask(task, t => { if (NetworkedTask.TryGet(t, out var netTask)) netTask.UpdateDestinationTrack(); }); + } + + private void OnJobCarChanged((Job, Car) jct) + { + var (job, car) = jct; + if (job.ID == Job.ID) foreach (var task in job.tasks) NetworkedTask.DoOnActualTask(task, t => { if (NetworkedTask.TryGet(t, out var netTask)) netTask.UpdateCar(car); }); + } + public void AddReport(NetworkedItem item) { if (item == null || !item.UsefulItem) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs index a938b837..213a0e86 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs @@ -1,4 +1,6 @@ using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking.Train; using System; using System.Collections.Generic; @@ -46,6 +48,18 @@ public static bool TryGetNetId(Task task, out ushort netId) } #endregion + public static void DoOnActualTask(Task task, Action action) + { + if (task is ParallelTasks || task is SequentialTasks) + { + Traverse.Create(task) + .Field("tasks") + .GetValue>() + .Do(t => DoOnActualTask(t, action)); + } + action(task); + } + protected override bool IsIdServerAuthoritative => true; public Task Task { get; private set; } @@ -85,13 +99,24 @@ protected override void OnDestroy() public void SetState(TaskState newState) { - if (lastState == newState && lastStartTime == Task.taskStartTime && lastFinishTime == Task.taskFinishTime) + if ((lastState == newState && lastStartTime == Task.taskStartTime && lastFinishTime == Task.taskFinishTime) || !NetworkedJob.TryGetNetId(Task.Job, out var jobNetId)) return; lastState = newState; lastStartTime = Task.taskStartTime; lastFinishTime = Task.taskFinishTime; - NetworkLifecycle.Instance.Server.SendTaskUpdate(NetId, newState, Task.taskStartTime, Task.taskFinishTime); + NetworkLifecycle.Instance.Server.SendTaskUpdate(jobNetId, NetId, newState, Task.taskStartTime, Task.taskFinishTime); + } + + public void UpdateDestinationTrack() + { + var destTrackOrNull = Traverse.Create(Task).Field("destinationTrack").GetValue(); + if (destTrackOrNull != null && NetworkedJob.TryGetNetId(Task.Job, out var jobNetId)) NetworkLifecycle.Instance.Server.SendTaskDestTrackUpdate(jobNetId, NetId, destTrackOrNull); + } + + public void UpdateCar(Car car) + { + if (NetworkedTrainCar.TryGetNetId(car, out var carNetId) && NetworkedJob.TryGetNetId(Task.Job, out var jobNetId)) NetworkLifecycle.Instance.Server.SendTaskCarUpdate(jobNetId, NetId, carNetId); } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 7516e6c0..65657205 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -166,7 +166,7 @@ private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bo HandleCoupling(spawnPart.RearCoupling, trainCar.rearCoupler); } - private static void HandleCoupling(CouplingData couplingData, Coupler currentCoupler) + public static void HandleCoupling(CouplingData couplingData, Coupler currentCoupler) { CouplingData cd = couplingData; diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 2e7fd95b..4d6d574c 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -195,8 +195,8 @@ static string GetFuse(uint netId) private bool cargoIsLoading; public byte CargoModelIndex = byte.MaxValue; private bool carHealthDirty; - private bool sendCouplers; - private bool sendCables; + public bool sendCouplers; + public bool sendCables; public bool IsDestroying; @@ -875,7 +875,7 @@ private void Server_SendBrakeStates() ); } - private void Server_SendCouplers() + public void Server_SendCouplers() { if (!sendCouplers) return; @@ -906,7 +906,7 @@ private void Server_SendCouplers() NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); } - private void Server_SendCables() + public void Server_SendCables() { if (!sendCables) return; diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 864eb512..3ba1c471 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -7,10 +7,12 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Networking.Data.Items; using Multiplayer.Networking.Data.Jobs; +using Multiplayer.Networking.Managers; using Multiplayer.Utils; using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -167,6 +169,9 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta } #endregion + public static readonly HashSet WaitingNSCs = []; + public static readonly HashSet DelayedJobs = []; + const int MAX_FRAMES = 120; protected override bool IsIdServerAuthoritative => false; @@ -303,7 +308,7 @@ private void Server_OnTick(uint tick) #endregion Server #region Client - public void AddJobs(JobData[] jobs) + public void AddJobs(JobData[] jobs, bool noTimeout = false) { foreach (JobData jobData in jobs) @@ -317,7 +322,8 @@ public void AddJobs(JobData[] jobs) else { Multiplayer.LogDebug(() => $"AddJobs() Delaying({jobData.ID})"); - StartCoroutine(DelayCreateJob(jobData)); + DelayedJobs.Add(jobData.NetID); + StartCoroutine(DelayCreateJob(jobData, noTimeout)); } } } @@ -332,7 +338,11 @@ private void AddJob(JobData jobData) var carNetIds = jobData.GetCars(); NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds, netIdToTask); - NetworkedJobs.Add(networkedJob); + if (!NetworkedJobs.Add(networkedJob)) + { + Multiplayer.LogWarning($"Net Job {jobData.ID} already present, not adding!"); + return; + } if (networkedJob.Job.State == JobState.Available) { @@ -364,17 +374,18 @@ private void AddJob(JobData jobData) StartCoroutine(UpdateCarPlates(carNetIds, newJob.ID)); Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); + DelayedJobs.Remove(jobData.NetID); } - private IEnumerator DelayCreateJob(JobData jobData) + private IEnumerator DelayCreateJob(JobData jobData, bool noTimeout = false) { int frameCounter = 0; Multiplayer.LogDebug(() => $"DelayCreateJob([{jobData.NetID}, {jobData.ID}]) job type: {jobData.JobType}"); - yield return new WaitForEndOfFrame(); + yield return WaitFor.EndOfFrame; - while (frameCounter < MAX_FRAMES) + while (noTimeout || frameCounter < MAX_FRAMES) { if (CheckCarsLoaded(jobData)) { @@ -384,10 +395,11 @@ private IEnumerator DelayCreateJob(JobData jobData) } frameCounter++; - yield return new WaitForEndOfFrame(); + yield return WaitFor.EndOfFrame; } Multiplayer.LogWarning($"Timeout waiting for cars to load for job [{jobData.NetID}, {jobData.ID}]"); + DelayedJobs.Remove(jobData.NetID); } private bool CheckCarsLoaded(JobData jobData) @@ -416,20 +428,43 @@ private NetworkedJob CreateNetworkedJob(Job job, ushort netId, List carN return networkedJob; } - public void UpdateJobs(JobUpdateStruct[] jobs) + public void AskServerForAdditionalJobs(bool generateJobs) { - foreach (JobUpdateStruct job in jobs) - { - if (!NetworkedJob.Get(job.JobNetID, out NetworkedJob netJob)) - continue; + var present = NetworkedJobs.Select(nj => nj.NetId).ToArray(); + NetworkLifecycle.Instance.Client.SendJobsRequest(this, present, generateJobs); + } + + public IEnumerator UpdateJobs(uint stationNetId, JobUpdateStruct[] jobs) + { + Coroutine[] coroutines = jobs.Select(j => StartCoroutine(UpdateJob(stationNetId, j))).ToArray(); + foreach (var coroutine in coroutines) yield return coroutine; + } - netJob.Job.startTime = job.StartTime; - netJob.Job.finishTime = job.FinishTime; + public IEnumerator UpdateJob(uint stationNetId, JobUpdateStruct job) + { + int frameCounter = 0; + + if (DelayedJobs.Contains(job.JobNetID)) Multiplayer.LogDebug(() => $"Job with netId {job.JobNetID} is delayed in its creation, will wait with updating"); + while (frameCounter < 600 && DelayedJobs.Contains(job.JobNetID)) + { + frameCounter++; + yield return WaitFor.EndOfFrame; + } - UpdateJobState(netJob, job); - UpdateJobOverview(netJob, job); + if (frameCounter >= 600) Multiplayer.LogWarning($"NetworkedStationController.UpdateJob timed out waiting for delayed job with netId {job.JobNetID}"); + if (!NetworkedJob.Get(job.JobNetID, out NetworkedJob netJob)) + { + NetworkLifecycle.Instance.Client.LogWarning($"Unknown or invalid jobNetId {job.JobNetID}, aksing server to re-send jobs and skipping update!"); + AskServerForAdditionalJobs(false); + yield break; } + + netJob.Job.startTime = job.StartTime; + netJob.Job.finishTime = job.FinishTime; + + UpdateJobState(netJob, job); + UpdateJobOverview(netJob, job); } private void UpdateJobState(NetworkedJob netJob, JobUpdateStruct job) @@ -608,7 +643,7 @@ public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Incrementing frames"); frameCounter++; - yield return new WaitForEndOfFrame(); + yield return WaitFor.EndOfFrame; } if (frameCounter >= MAX_FRAMES) diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 26773f76..1586bbeb 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,5 +1,7 @@ using DV; +using DV.Logic.Job; using DV.UIFramework; +using DV.Utils; using HarmonyLib; using JetBrains.Annotations; using LiteNetLib; @@ -11,6 +13,7 @@ using Multiplayer.Patches.Mods; using Multiplayer.Patches.World; using System; +using System.Collections; using System.IO; using System.Linq; using System.Reflection; @@ -49,6 +52,10 @@ public static string Ver { public static bool specLog = false; + public static bool PersJobs = false; + public static MethodInfo PersJobsJobTrackChangedEventRegMethod; + public static MethodInfo PersJobsJobCarChangedEventRegMethod; + [UsedImplicitly] public static bool Load(UnityModManager.ModEntry modEntry) { @@ -103,6 +110,8 @@ public static bool Load(UnityModManager.ModEntry modEntry) RemoteDispatchPatch.Patch(harmony, remoteDispatch.Assembly); } + TryLoadPersistentJobs(); + if (!LoadAssets()) return false; @@ -164,6 +173,44 @@ public static bool LoadAssets() return true; } + private static void TryLoadPersistentJobs() + { + UnityModManager.ModEntry persistentJobs = UnityModManager.FindMod("PersistentJobsMod"); + if (persistentJobs?.Enabled == true) + { + Log("Found Persistent Jobs, waiting for it to load"); + SingletonBehaviour.Instance.Run(WaitForPersistentJobsAndLoad(persistentJobs)); + } + } + + private static IEnumerator WaitForPersistentJobsAndLoad(UnityModManager.ModEntry persistentJobs) + { + float timeout = 1000f; + float start = Time.realtimeSinceStartup; + + yield return new WaitUntil(() => persistentJobs?.Loaded == true || Time.realtimeSinceStartup - start > timeout); + + if (!persistentJobs.Loaded) + { + Log("Timed out waiting for PersistentJobs."); + yield break; + } + + try + { + LogDebug(() => "Loading compat with PersistentJobs"); + + PersJobsJobTrackChangedEventRegMethod = AccessTools.Method(AccessTools.TypeByName("PersistentJobsMod.ModInteraction.PersistentJobsModInteractionFeatures"), "RegisterJobTracksChangedListener", [typeof(Action)]); + PersJobsJobCarChangedEventRegMethod = AccessTools.Method(AccessTools.TypeByName("PersistentJobsMod.ModInteraction.PersistentJobsModInteractionFeatures"), "RegisterJobCarsChangedListener", [typeof(Action<(Job, Car)>)]); + PersJobs = true; + } + catch + { + LogWarning("Persistent Jobs load failed"); + PersJobs = false; + } + } + private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTime) { if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index a19ae156..96fd43eb 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -10,7 +10,9 @@ using DV.ThingTypes; using DV.UI; using DV.UserManagement; +using DV.Utils; using DV.WeatherSystem; +using HarmonyLib; using LiteNetLib; using LiteNetLib.Utils; using MPAPI.Interfaces.Packets; @@ -47,6 +49,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Unity.Jobs; using UnityEngine; using Object = UnityEngine.Object; @@ -73,6 +76,13 @@ public class NetworkClient : NetworkManager internal uint trainSetsToSpawn = uint.MaxValue; internal uint trainSetsSpawned = 0; internal bool railwayStateLoaded = false; + internal bool noCarTimeout = false; + + private Queue ClientboundTaskUpdateQueue = new(); + private bool taskUpdateProcessing; + + private Queue ClientboundJobUpdateQueue = new(); + private bool jobUpdateProcessing; // One way ping in milliseconds public int Ping { get; private set; } @@ -191,6 +201,7 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); + netPacketProcessor.SubscribeReusable(OnClientboudCouplingStatePacket); netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); @@ -897,6 +908,19 @@ private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet) coupler.IsCockOpen = packet.IsOpen; } + private void OnClientboudCouplingStatePacket(ClientboudCouplingStatePacket packet) + { + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) + return; + + LogDebug(() => $"OnClientboudCouplingStatePacket({trainCar.ID})"); + + NetworkedCarSpawner.HandleCoupling(packet.FrontCouplingData, trainCar.frontCoupler); + NetworkedCarSpawner.HandleCoupling(packet.RearCouplingData, trainCar.rearCoupler); + + if (trainCar.IsMultipleUnit) trainCar.muModule.MultipleUnitStateRestoreOnGameLoad(packet.FronMuConnected, packet.RearMuConnected); + } + private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet) { if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) @@ -1168,7 +1192,9 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) Log($"Received {packet.Jobs.Length} jobs for station {networkedStationController.StationController.logicStation.ID}"); - networkedStationController.AddJobs(packet.Jobs); + networkedStationController.AddJobs(packet.Jobs, noCarTimeout); + noCarTimeout = false; + NetworkedStationController.WaitingNSCs.Remove(networkedStationController.NetId); } private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) @@ -1176,15 +1202,44 @@ private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) if (NetworkLifecycle.Instance.IsHost()) return; - if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) + ClientboundJobUpdateQueue.Enqueue(packet); + if (!jobUpdateProcessing) { - LogError($"OnClientboundJobsUpdatePacket() {packet.StationNetId} does not exist!"); - return; + jobUpdateProcessing = true; + SingletonBehaviour.Instance.Run(ProcessClientboundJobUpdatePackets()); } - Log($"Received {packet.JobUpdates.Length} job updates for station {networkedStationController.StationController.logicStation.ID}"); + } + + private IEnumerator ProcessClientboundJobUpdatePackets() + { + while (ClientboundJobUpdateQueue.Count > 0) + { + var packet = ClientboundJobUpdateQueue.Dequeue(); + + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) + { + LogError($"OnClientboundJobsUpdatePacket() {packet.StationNetId} does not exist!"); + continue; + } + + Log($"Received {packet.JobUpdates.Length} job updates for station {networkedStationController.StationController.logicStation.ID}"); + + int waitingNSCsFrameCounter = 0; + + if (NetworkedStationController.WaitingNSCs.Contains(packet.StationNetId)) LogDebug(() => $"Station {networkedStationController.StationController.stationInfo.YardID} is waiting for new jobs, job updates will be processed later"); + while (waitingNSCsFrameCounter < 600 && NetworkedStationController.WaitingNSCs.Contains(packet.StationNetId)) + { + waitingNSCsFrameCounter++; + yield return null; + } + + if (waitingNSCsFrameCounter >= 600) LogWarning($"NetworkClient.ProcessClientboundJobUpdatePackets timed out waiting for NSC with netId {packet.StationNetId}"); + + yield return networkedStationController.UpdateJobs(packet.StationNetId, packet.JobUpdates); + } - networkedStationController.UpdateJobs(packet.JobUpdates); + jobUpdateProcessing = false; } private void OnClientboundTaskUpdatePacket(ClientboundTaskUpdatePacket packet) @@ -1192,15 +1247,98 @@ private void OnClientboundTaskUpdatePacket(ClientboundTaskUpdatePacket packet) if (NetworkLifecycle.Instance.IsHost()) return; - if (!NetworkedTask.TryGet(packet.TaskNetId, out Task task) || task == null) + ClientboundTaskUpdateQueue.Enqueue(packet); + if (!taskUpdateProcessing) { - LogError($"Received task update for taskNetId {packet.TaskNetId}, task was not found"); - return; + taskUpdateProcessing = true; + SingletonBehaviour.Instance.Run(ProcessClientboundTaskUpdatePackets()); } + } + + private IEnumerator ProcessClientboundTaskUpdatePackets() + { + try + { + while (ClientboundTaskUpdateQueue.Count > 0) + { + int waitingNSCsFrameCounter = 0; + + if (NetworkedStationController.WaitingNSCs.Any()) LogDebug(() => $"Stations are waiting for new jobs, task updates will be processed later"); + while (waitingNSCsFrameCounter < 600 && NetworkedStationController.WaitingNSCs.Any()) + { + waitingNSCsFrameCounter++; + yield return null; + } + + if (waitingNSCsFrameCounter >= 600) LogWarning("NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for waitingNSCs"); + + var packet = ClientboundTaskUpdateQueue.Dequeue(); - task.SetState(packet.NewState); - task.taskStartTime = packet.TaskStartTime; - task.taskFinishTime = packet.TaskFinishTime; + int delayedJobsWaitFrameCounter = 0; + if (NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) Multiplayer.LogDebug(() => $"Job with netId {packet.JobNetId} is delayed in its creation, will wait with task updating"); + while (delayedJobsWaitFrameCounter < 600 && NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) + { + delayedJobsWaitFrameCounter++; + yield return null; + } + + if (delayedJobsWaitFrameCounter >= 600) LogWarning($"NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for delayed job with netId {packet.JobNetId}"); + + if (!NetworkedTask.TryGet(packet.TaskNetId, out Task task) || task == null) + { + LogError($"Received task update for taskNetId {packet.TaskNetId}, task was not found"); + continue; + } + + if (packet.TaskStateUpdate == true) + { + task.SetState(packet.NewState); + task.taskStartTime = packet.TaskStartTime; + task.taskFinishTime = packet.TaskFinishTime; + } + + if (packet.ReplaceDestTrack == true) + { + var track = (NetworkedRailTrack.TryGet(packet.DestTrackId, out RailTrack railTrack) ? railTrack.LogicTrack() : null); + if (track != null) Traverse.Create(task).Field("destinationTrack").SetValue(track); + } + + if (packet.ReplaceCar == true) + { + int carWaitCounter = 0; + while (carWaitCounter < 600 && !NetworkedTrainCar.TryGet(packet.CarNetID, out Car _)) + { + carWaitCounter++; + yield return null; + } + + if (!NetworkedTrainCar.TryGet(packet.CarNetID, out Car car)) + { + LogError($"No car for netId {packet.CarNetID} found, skipping replacement!"); + continue; + } + + void ReplaceCar(Task t) + { + var cars = Traverse.Create(t).Field("cars").GetValue>(); + if (cars == null) return; + if (cars.Remove(cars.FirstOrDefault(c => c.ID == car.ID))) + { + cars.Add(car); + SingletonBehaviour.Instance.Run(NetworkedStationController.UpdateCarPlates([packet.CarNetID], t.Job.ID)); + } + } + + NetworkedTask.DoOnActualTask(task, ReplaceCar); + } + + yield return WaitFor.EndOfFrame; + } + } + finally + { + taskUpdateProcessing = false; + } } private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) @@ -1781,6 +1919,20 @@ public void SendWarehouseRequest(WarehouseAction action, ushort netId) }, DeliveryMethod.ReliableUnordered); } + public void SendJobsRequest(NetworkedStationController station, ushort[] alreadyPresent, bool generateJobs) + { + LogDebug(() => $"Client asking for jobs in {station.StationController.stationInfo.YardID}, excluding {alreadyPresent.Length} already present jobs "); + noCarTimeout = true; + NetworkedStationController.WaitingNSCs.Add(station.NetId); + + SendPacketToServer(new ServerboundJobsRequestPacket + { + StationNetId = station.NetId, + GenerateJobs = generateJobs, + ExcludeJobNetId = alreadyPresent + }, DeliveryMethod.ReliableUnordered); + } + public void SendChat(string message) { SendPacketToServer(new CommonChatPacket diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index d5061427..764f3981 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -6,6 +6,7 @@ using Multiplayer.Networking.Data.Jobs; using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Data.World; +using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Serialization; using Multiplayer.Networking.TransportLayers; using System; @@ -62,6 +63,7 @@ private void RegisterNestedTypes() netPacketProcessor.RegisterNestedType(StationsChainNetworkData.Serialize, StationsChainNetworkData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); + netPacketProcessor.RegisterNestedType(CouplingData.Serialize, CouplingData.Deserialize); netPacketProcessor.RegisterNestedType(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); netPacketProcessor.RegisterNestedType(PitStopPlugMappingData.Serialize, PitStopPlugMappingData.Deserialize); netPacketProcessor.RegisterNestedType(LocoResourceModuleData.Serialize, LocoResourceModuleData.Deserialize); diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a5dba339..a6703cbe 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -8,6 +8,7 @@ using DV.Scenarios.Common; using DV.ServicePenalty; using DV.ThingTypes; +using DV.Utils; using DV.WeatherSystem; using Humanizer; using LiteNetLib; @@ -40,6 +41,7 @@ using Multiplayer.Patches.MainMenu; using Multiplayer.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; @@ -210,6 +212,7 @@ protected override void Subscribe() // Jobs netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundJobsRequestPacket); netPacketProcessor.SubscribeReusable(OnServerboundWarehouseMachineControllerRequestPacket); // Items @@ -887,6 +890,41 @@ public void SendCockState(ushort netId, Coupler coupler, bool isOpen) ); } + public void SendAbsoluteCouplingStatus(TrainCar trainCar) + { + if (!NetworkedTrainCar.TryGetNetId(trainCar, out ushort netId)) + { + LogWarning($"TrainCar {trainCar.ID} doesn´t have a walid networked counterpart"); + return; + } + + LogDebug(() => $"SendAbsoluteCouplingStatus({trainCar.ID})"); + + bool frontMu = false; + bool rearMu = false; + + if (trainCar.IsMultipleUnit) + { + frontMu = trainCar.muModule.frontCable.IsConnected; + rearMu = trainCar.muModule.rearCable.IsConnected; + } + + SendPacketToAll + ( + new ClientboudCouplingStatePacket + { + NetId = netId, + FrontCouplingData = CouplingData.From(trainCar.frontCoupler), + RearCouplingData = CouplingData.From(trainCar.rearCoupler), + FronMuConnected = frontMu, + RearMuConnected = rearMu + }, + DeliveryMethod.ReliableOrdered, + PlayerLoadingState.ReadyForTrainSets, + true + ); + } + public void SendTrainControlAuthorityUpdate(ushort netId, uint portNetId, ControlAuthorityState state, ServerPlayer sendToPlayer = null, ServerPlayer excludePlayer = null) { var packet = new ClientboundTrainControlAuthorityUpdatePacket @@ -923,17 +961,55 @@ public void SendJobsUpdatePacket(uint stationNetId, NetworkedJob[] jobs) SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, PlayerLoadingState.ReadyForJobs, excludeSelf: true); } - public void SendTaskUpdate(ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) + public void SendTaskUpdate(ushort jobNetId, ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) { Multiplayer.Log($"Sending TaskUpdate for taskNetId {taskNetId}, newState {newState}"); SendPacketToAll ( new ClientboundTaskUpdatePacket { + JobNetId = jobNetId, TaskNetId = taskNetId, NewState = newState, TaskStartTime = taskStartTime, - TaskFinishTime = taskFinishTime + TaskFinishTime = taskFinishTime, + TaskStateUpdate = true + }, + DeliveryMethod.ReliableOrdered, + PlayerLoadingState.ReadyForJobs, + excludeSelf: true + ); + } + + public void SendTaskDestTrackUpdate(ushort jobNetId, ushort taskNetId, Track newDestTrack) + { + Multiplayer.Log($"Sending TaskDestTrackUpdate for taskNetId {taskNetId}, new track is {newDestTrack.ID}"); + SendPacketToAll + ( + new ClientboundTaskUpdatePacket + { + JobNetId = jobNetId, + TaskNetId = taskNetId, + DestTrackId = newDestTrack.RailTrack().Networked().NetId, + ReplaceDestTrack = true + }, + DeliveryMethod.ReliableOrdered, + PlayerLoadingState.ReadyForJobs, + excludeSelf: true + ); + } + + public void SendTaskCarUpdate(ushort jobNetId, ushort taskNetId, ushort carNetId) + { + Multiplayer.Log($"Sending TaskCarUpdate for taskNetId {taskNetId}, car netId is {carNetId}"); + SendPacketToAll + ( + new ClientboundTaskUpdatePacket + { + JobNetId = jobNetId, + TaskNetId = taskNetId, + CarNetID = carNetId, + ReplaceCar = true }, DeliveryMethod.ReliableOrdered, PlayerLoadingState.ReadyForJobs, @@ -1958,6 +2034,34 @@ private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequest //SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = false }, DeliveryMethod.ReliableUnordered); } + private void OnServerboundJobsRequestPacket(ServerboundJobsRequestPacket packet, ITransportPeer peer) + { + LogDebug(() => $"ServerboundJobsRequestPacket for station {packet.StationNetId}"); + + if (!NetworkedStationController.Get(packet.StationNetId, out var networkedStationController)) + { + LogWarning($"ServerboundJobsRequestPacket() invalid station net id {packet.StationNetId}"); + return; + } + + if (packet.GenerateJobs) + { + LogDebug(() => $"ServerboundJobsRequestPacket requested job generation for station {packet.StationNetId}"); + networkedStationController.StationController.ProceduralJobsController.TryToGenerateJobs(); + } + + SingletonBehaviour.Instance.Run(SendJobsToClientsOnRequest(packet, networkedStationController)); + } + + private IEnumerator SendJobsToClientsOnRequest(ServerboundJobsRequestPacket packet, NetworkedStationController networkedStationController) + { + if (networkedStationController.StationController.ProceduralJobsController.IsJobGenerationActive) LogDebug(() => $"Station {networkedStationController.StationController.stationInfo.YardID} is still generating jobs, will wait with sending"); + while (networkedStationController.StationController.ProceduralJobsController.IsJobGenerationActive) yield return null; + + var send = networkedStationController.NetworkedJobs.Where(nj => !packet.ExcludeJobNetId.Contains(nj.NetId)).ToArray(); + NetworkLifecycle.Instance.Server.SendJobsCreatePacket(networkedStationController, send); + } + private void OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWarehouseMachineControllerRequestPacket packet, ITransportPeer peer) { LogDebug(() => $"ServerboundWarehouseMachineControllerRequestPacket(): {packet.NetId}"); diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs index abf7ff5d..560a090f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs @@ -4,8 +4,14 @@ namespace Multiplayer.Networking.Packets.Clientbound.Jobs; internal class ClientboundTaskUpdatePacket { + public ushort JobNetId { get; set; } public ushort TaskNetId { get; set; } public TaskState NewState { get; set; } public float TaskStartTime { get; set; } public float TaskFinishTime { get; set; } + public bool TaskStateUpdate { get; set; } + public ushort CarNetID { get; set; } + public bool ReplaceCar { get; set; } + public ushort DestTrackId { get; set; } + public bool ReplaceDestTrack { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboudCouplingStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboudCouplingStatePacket.cs new file mode 100644 index 00000000..e93e4264 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboudCouplingStatePacket.cs @@ -0,0 +1,12 @@ +using Multiplayer.Networking.Data.Train; + +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboudCouplingStatePacket +{ + public ushort NetId { get; set; } + public CouplingData FrontCouplingData { get; set; } + public CouplingData RearCouplingData { get; set; } + public bool FronMuConnected { get; set; } + public bool RearMuConnected { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs new file mode 100644 index 00000000..66c6496d --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs @@ -0,0 +1,10 @@ +using Multiplayer.Networking.Data.Jobs; + +namespace Multiplayer.Networking.Packets.Serverbound.Jobs; + +public class ServerboundJobsRequestPacket +{ + public uint StationNetId { get; set; } + public bool GenerateJobs { get; set; } + public ushort[] ExcludeJobNetId { get; set; } +} diff --git a/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs index 0d82e62d..112ac4a1 100644 --- a/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs @@ -1,13 +1,31 @@ using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using System.Linq; namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(StationProceduralJobsController), nameof(StationProceduralJobsController.TryToGenerateJobs))] public static class StationProceduralJobsController_TryToGenerateJobs_Patch { - private static bool Prefix() + private static bool Prefix(StationProceduralJobsController __instance) { + if (NetworkedStationController.GetFromStationController(__instance.stationController, out var networkedStationController)) + { + if (NetworkLifecycle.Instance.IsHost()) + { + if (!networkedStationController.StationController.ProceduralJobsController.IsJobGenerationActive) + { + var jobsToSend = __instance.jobChainControllers.Select(jcc => jcc.currentJobInChain).ToList(); + jobsToSend.RemoveAll(j => networkedStationController.NetworkedJobs.Any(nj => nj.Job.ID == j.ID)); + if (jobsToSend.Any()) jobsToSend.ForEach(j => networkedStationController.AddJob(j)); + } + } + else + { + networkedStationController.AskServerForAdditionalJobs(true); + } + } return NetworkLifecycle.Instance.IsHost(); } } diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index bd291536..175cfecd 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -77,4 +77,22 @@ private static void SpawnCarOnClosestTrack(TrainCar __result) NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], true, true); } + + //gets triggered on save load or by PersJobs only + [HarmonyPatch(nameof(CarSpawner.SpawnLoadedCar))] + [HarmonyPostfix] + private static void SpawnLoadedCar(TrainCar __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null) + return; + + Multiplayer.LogDebug(() => $"SpawnLoadedCar() {__result?.carLivery?.name} spawned, sending to players"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], false, true); + } } diff --git a/Multiplayer/Patches/World/CarsSaveManagerPatch.cs b/Multiplayer/Patches/World/CarsSaveManagerPatch.cs index d8851293..8d1bffc4 100644 --- a/Multiplayer/Patches/World/CarsSaveManagerPatch.cs +++ b/Multiplayer/Patches/World/CarsSaveManagerPatch.cs @@ -1,16 +1,69 @@ +using DV.JObjectExtstensions; +using DV.Utils; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data.Train; +using Newtonsoft.Json.Linq; +using System.Collections; +using UnityEngine; namespace Multiplayer.Patches.World; -[HarmonyPatch(typeof(CarsSaveManager), nameof(CarsSaveManager.Load))] -public static class CarsSaveManager_Load_Patch +[HarmonyPatch(typeof(CarsSaveManager))] +public static class CarsSaveManager_Patch { - private static bool Prefix() + [HarmonyPatch(nameof(CarsSaveManager.Load))] + [HarmonyPrefix] + private static bool Load_Prefix() { if (!NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsHost()) return true; CarsSaveManager.DeleteAllExistingCars(); return false; } + + [HarmonyPatch(nameof(CarsSaveManager.RestoreCarConnections))] + [HarmonyPostfix] + private static void RestoreCarConnections_Postfix(JObject carData) + { + TrainCar trainCarByCarGuid = SingletonBehaviour.Instance.GetTrainCarByCarGuid(carData.GetString("carGuid")); + if (WorldStreamingInit.IsLoaded && trainCarByCarGuid != null && NetworkedTrainCar.TryGetFromTrainCar(trainCarByCarGuid, out var networkedTrainCar)) + { + NetworkLifecycle.Instance.Server.SendAbsoluteCouplingStatus(trainCarByCarGuid); + } + //SingletonBehaviour.Instance.Run(SendConnectionsForRestoredCarDelayed(carData)); + } + + private static IEnumerator SendConnectionsForRestoredCarDelayed(JObject carData) + { + yield return WaitFor.SecondsRealtime(0.25f); + + TrainCar trainCarByCarGuid = SingletonBehaviour.Instance.GetTrainCarByCarGuid(carData.GetString("carGuid")); + if (trainCarByCarGuid != null && NetworkedTrainCar.TryGetFromTrainCar(trainCarByCarGuid, out var networkedTrainCar)) + { + NetworkLifecycle.Instance.Server.SendAbsoluteCouplingStatus(trainCarByCarGuid); + + /*var frontOtherCoupler = trainCarByCarGuid.frontCoupler.GetAirHoseConnectedTo(); + if (frontOtherCoupler != null && (!(carData.GetBool("airHoseF") == true) && !(carData.GetBool("airCockF") == true))) + { + frontOtherCoupler.IsCockOpen = false; + frontOtherCoupler.DisconnectAirHose(false); + } + + var rearOtherCoupler = trainCarByCarGuid.frontCoupler.GetAirHoseConnectedTo(); + if (rearOtherCoupler != null && (!(carData.GetBool("airHoseR") == true) && !(carData.GetBool("airCockR") == true))) + { + rearOtherCoupler.IsCockOpen = false; + rearOtherCoupler.DisconnectAirHose(false); + } + + yield return null; + + networkedTrainCar.sendCouplers = true; + networkedTrainCar.sendCables = true; + networkedTrainCar.Server_SendCouplers(); + networkedTrainCar.Server_SendCables();*/ + } + } } From 38f7c4f00ea4df4fe419a5efc54a13401e7a8cb3 Mon Sep 17 00:00:00 2001 From: T3M8029 <102697658+T3M8029@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:45:34 +0200 Subject: [PATCH 2/4] Fix possible race conditions on jobs updating --- .../Networking/Jobs/NetworkedJob.cs | 39 ++++- .../World/NetworkedStationController.cs | 134 +++++++++++++----- Multiplayer/Multiplayer.cs | 6 + .../Managers/Client/NetworkClient.cs | 79 ++++++----- .../Managers/Server/NetworkServer.cs | 36 ++++- .../Jobs/ClientboundJobsUpdatePacket.cs | 3 +- .../Jobs/ClientboundTaskUpdatePacket.cs | 4 +- .../Jobs/ServerboundJobsRequestPacket.cs | 1 + .../StationProceduralJobsControllerPatch.cs | 4 +- Multiplayer/Utils/CombinedTypes.cs | 87 ++++++++++++ 10 files changed, 310 insertions(+), 83 deletions(-) create mode 100644 Multiplayer/Utils/CombinedTypes.cs diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 56f4828f..1a7b891d 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -1,11 +1,13 @@ using DV.CabControls; using DV.InventorySystem; using DV.Logic.Job; +using DV.Utils; using HarmonyLib; using Multiplayer.API; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data.Jobs; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -130,6 +132,9 @@ public NetworkedItem JobReport private readonly List JobReports = []; + private object[] OnJobTrackChengeEventRegistrator; + private object[] OnJobCarChangedEventRegistrator; + public Guid OwnedBy { get; set; } = Guid.Empty; public JobValidator JobValidator { get; set; } @@ -178,8 +183,10 @@ public void Initialize(Job job, NetworkedStationController station) if (Multiplayer.PersJobs) { - Multiplayer.PersJobsJobTrackChangedEventRegMethod.Invoke(null, [(Action)OnJobTrackChanged]); - Multiplayer.PersJobsJobCarChangedEventRegMethod.Invoke(null, [(Action<(Job, Car)>)OnJobCarChanged]); + OnJobTrackChengeEventRegistrator = [(Action)OnJobTrackChanged]; + OnJobCarChangedEventRegistrator = [(Action<(Job, Car)>)OnJobCarChanged]; + Multiplayer.PersJobsJobTrackChangedEventRegMethod.Invoke(null, OnJobTrackChengeEventRegistrator); + Multiplayer.PersJobsJobCarChangedEventRegMethod.Invoke(null, OnJobCarChangedEventRegistrator); } // If this is called after Start(), we need to add to cache here @@ -293,8 +300,28 @@ private void OnJobTrackChanged(Job job) private void OnJobCarChanged((Job, Car) jct) { + Multiplayer.LogDebug(() => $"OnJobCarChanged() fired for {jct.Item2.ID} in {jct.Item1.ID}"); + SingletonBehaviour.Instance.Run(OnJobCarChangedDelayed(jct)); + } + + private IEnumerator OnJobCarChangedDelayed((Job, Car) jct) + { + if (Multiplayer.PersJobs) + { + if ((bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null)) Multiplayer.LogDebug(() => $"Cars still resuming, waiting with job car changes"); + //while ((bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null)) yield return WaitFor.EndOfFrame; + yield return new WaitUntil(() => (bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null) == false); + } + yield return null; var (job, car) = jct; if (job.ID == Job.ID) foreach (var task in job.tasks) NetworkedTask.DoOnActualTask(task, t => { if (NetworkedTask.TryGet(t, out var netTask)) netTask.UpdateCar(car); }); + yield break; + } + + public bool TryGetNetworkedStationControllerHandlingNetworkedJob(out NetworkedStationController networkedStationController) + { + networkedStationController = NetworkedStationController.stationControllerToNetworkedStationController.Values.ToArray().FirstOrDefault(sc => sc.NetworkedJobs.Contains(this)); + return (networkedStationController == null) ? false : true; } public void AddReport(NetworkedItem item) @@ -377,6 +404,14 @@ protected void OnDisable() Job.JobCompleted -= OnJobCompleted; Job.JobExpired -= OnJobExpired; + if (Multiplayer.PersJobs) + { + Multiplayer.PersJobsJobTrackChangedEventRegMethod.Invoke(null, OnJobTrackChengeEventRegistrator); + Multiplayer.PersJobsJobCarChangedEventUnregMethod.Invoke(null, OnJobCarChangedEventRegistrator); + OnJobTrackChengeEventRegistrator = null; + OnJobCarChangedEventRegistrator = null; + } + Destroy(this); } diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index 3ba1c471..f13120f5 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -13,6 +13,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using UnityEngine; namespace Multiplayer.Components.Networking.World; @@ -20,7 +21,7 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedStationController : IdMonoBehaviour { #region Lookup Cache - private static readonly Dictionary stationControllerToNetworkedStationController = []; + public static readonly Dictionary stationControllerToNetworkedStationController = []; private static readonly Dictionary stationIdToNetworkedStationController = []; private static readonly Dictionary stationIdToStationController = []; private static readonly Dictionary stationToNetworkedStationController = []; @@ -172,6 +173,8 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta public static readonly HashSet WaitingNSCs = []; public static readonly HashSet DelayedJobs = []; + private readonly HashSetQueue CarPlateUpdates = new(); + const int MAX_FRAMES = 120; protected override bool IsIdServerAuthoritative => false; @@ -180,9 +183,12 @@ private static void RegisterJobValidator(JobValidator jobValidator, NetworkedSta public JobValidator JobValidator; + private Coroutine UpdateCarPlatesRoutine; + public HashSet NetworkedJobs { get; } = []; private readonly List NewJobs = []; private readonly List DirtyJobs = []; + public readonly HashSet TimeoutJobs = []; private List availableJobs; private List takenJobs; @@ -195,6 +201,7 @@ protected override void Awake() base.Awake(); StationController = GetComponent(); StartCoroutine(WaitForLogicStation()); + if (!NetworkLifecycle.Instance.IsHost()) UpdateCarPlatesRoutine = StartCoroutine(UpdateCarPlatesCoro()); } protected void Start() @@ -203,16 +210,27 @@ protected void Start() { NetworkLifecycle.Instance.OnTick += Server_OnTick; } + else + { + NetworkLifecycle.Instance.OnTick += Client_OnTick; + } } protected void OnDisable() { + if (UpdateCarPlatesRoutine is not null) StopCoroutine(UpdateCarPlatesRoutine); if (UnloadWatcher.isQuitting) return; if (NetworkLifecycle.Instance.IsHost()) + { NetworkLifecycle.Instance.OnTick -= Server_OnTick; + } + else + { + NetworkLifecycle.Instance.OnTick -= Client_OnTick; + } if (StationController != null) { @@ -308,11 +326,28 @@ private void Server_OnTick(uint tick) #endregion Server #region Client + + private void Client_OnTick(uint tick) + { + //Try get previously timed out jobs if player is in station (not on every tick) + if ((TimeoutJobs.Count > 0) && tick % 50 == 0 && StationController.playerEnteredJobGenerationZone) + { + NetworkLifecycle.Instance.Client.SendJobsRequest(this, [], false, TimeoutJobs.ToArray()); + } + } + public void AddJobs(JobData[] jobs, bool noTimeout = false) { foreach (JobData jobData in jobs) { + //check for duplicates and not let them pass --> !!!TODO: this shouldn´t be neccesarry - need to find reason for second send of the jobs + if (NetworkedJobs.Any(nj => nj.Job.ID == jobData.ID)) + { + Multiplayer.LogWarning($"Client already has job {jobData.ID}, not adding second copy"); + continue; + } + //Cars may still be loading, we shouldn't spawn the job until they are ready if (CheckCarsLoaded(jobData)) { @@ -371,21 +406,23 @@ private void AddJob(JobData jobData) Multiplayer.LogDebug(() => $"AddJob({jobData.ID}) Starting plate update {newJob.ID} count: {jobData.GetCars().Count}"); - StartCoroutine(UpdateCarPlates(carNetIds, newJob.ID)); + UpdateCarPlates(carNetIds, newJob.ID); Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); DelayedJobs.Remove(jobData.NetID); + TimeoutJobs.Remove(jobData.NetID); } private IEnumerator DelayCreateJob(JobData jobData, bool noTimeout = false) { int frameCounter = 0; + int maxWait = noTimeout ? 300 : MAX_FRAMES; Multiplayer.LogDebug(() => $"DelayCreateJob([{jobData.NetID}, {jobData.ID}]) job type: {jobData.JobType}"); - yield return WaitFor.EndOfFrame; + yield return null; - while (noTimeout || frameCounter < MAX_FRAMES) + while (noTimeout || frameCounter < maxWait) { if (CheckCarsLoaded(jobData)) { @@ -395,10 +432,11 @@ private IEnumerator DelayCreateJob(JobData jobData, bool noTimeout = false) } frameCounter++; - yield return WaitFor.EndOfFrame; + yield return null; } Multiplayer.LogWarning($"Timeout waiting for cars to load for job [{jobData.NetID}, {jobData.ID}]"); + TimeoutJobs.Add(jobData.NetID); DelayedJobs.Remove(jobData.NetID); } @@ -431,7 +469,7 @@ private NetworkedJob CreateNetworkedJob(Job job, ushort netId, List carN public void AskServerForAdditionalJobs(bool generateJobs) { var present = NetworkedJobs.Select(nj => nj.NetId).ToArray(); - NetworkLifecycle.Instance.Client.SendJobsRequest(this, present, generateJobs); + NetworkLifecycle.Instance.Client.SendJobsRequest(this, present, generateJobs, []); } public IEnumerator UpdateJobs(uint stationNetId, JobUpdateStruct[] jobs) @@ -558,7 +596,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat printed = true; } - StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); + UpdateCarPlates(netJob.JobCars, string.Empty); netJob.DestroyJobBooklet(); @@ -568,14 +606,14 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat takenJobs.Remove(netJob.Job); abandonedJobs.Add(netJob.Job); netJob.Job.AbandonJob(); - StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); + UpdateCarPlates(netJob.JobCars, string.Empty); break; case JobState.Expired: netJob.Job.ExpireJob(); netJob.DestroyJobOverview(); - StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); + UpdateCarPlates(netJob.JobCars, string.Empty); break; default: @@ -615,41 +653,71 @@ public void RemoveJob(NetworkedJob job) GameObject.Destroy(job); } - public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) + public void UpdateCarPlates(List carNetIds, string jobId) { + if (!CarPlateUpdates.Enqueue(new(carNetIds, jobId))) Multiplayer.LogDebug(() => $"Car plate update to {jobId} for cars with netIds {string.Join(", ", carNetIds)} alredy scheduled!"); + } - Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) carNetIds: {carNetIds?.Count}"); - - if (carNetIds == null || string.IsNullOrEmpty(jobId)) - yield break; - - foreach (ushort carNetId in carNetIds) + public IEnumerator UpdateCarPlatesCoro() + { + while (this.isActiveAndEnabled == true) { - int frameCounter = 0; - TrainCar trainCar = null; + yield return null; - while (frameCounter < MAX_FRAMES) + if (!StationController.playerEnteredJobGenerationZone) { + yield return WaitFor.Seconds(30f); + continue; + } - if (NetworkedTrainCar.TryGet(carNetId, out trainCar) && - trainCar != null && - trainCar.trainPlatesCtrl?.trainCarPlates != null && - trainCar.trainPlatesCtrl.trainCarPlates.Count > 0) + if (!CarPlateUpdates.TryDequeue(out var cpu)) + { + yield return null; + continue; + } + else + { + var (carNetIds, jobId) = cpu; + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) carNetIds: {carNetIds?.Count()}"); + + if (carNetIds == null) { - //Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Calling Update"); - trainCar.UpdateJobIdOnCarPlates(jobId); - break; + yield return null; + continue; } - Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Incrementing frames"); - frameCounter++; - yield return WaitFor.EndOfFrame; + foreach (ushort carNetId in carNetIds) + { + int frameCounter = 0; + TrainCar trainCar = null; + + while (frameCounter < MAX_FRAMES) + { + + if (NetworkedTrainCar.TryGet(carNetId, out trainCar) && + trainCar != null && + trainCar.trainPlatesCtrl?.trainCarPlates != null && + trainCar.trainPlatesCtrl.trainCarPlates.Count > 0) + { + //Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Calling Update"); + trainCar.UpdateJobIdOnCarPlates(jobId); + break; + } + + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Incrementing frames"); + frameCounter++; + yield return WaitFor.EndOfFrame; + } + + if (frameCounter >= MAX_FRAMES) + { + Multiplayer.LogError($"Failed to update plates for car [{trainCar?.ID}, {carNetId}] (Job: {jobId}) after {frameCounter} frames"); + yield return null; + } + } } - if (frameCounter >= MAX_FRAMES) - { - Multiplayer.LogError($"Failed to update plates for car [{trainCar?.ID}, {carNetId}] (Job: {jobId}) after {frameCounter} frames"); - } + yield return null; } } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 1586bbeb..76ed1f8c 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -54,7 +54,10 @@ public static string Ver { public static bool PersJobs = false; public static MethodInfo PersJobsJobTrackChangedEventRegMethod; + public static MethodInfo PersJobsJobTrackChangedEventUnregMethod; public static MethodInfo PersJobsJobCarChangedEventRegMethod; + public static MethodInfo PersJobsJobCarChangedEventUnregMethod; + public static FieldInfo PersJobsResumeCoroRunningField; [UsedImplicitly] public static bool Load(UnityModManager.ModEntry modEntry) @@ -201,7 +204,10 @@ private static IEnumerator WaitForPersistentJobsAndLoad(UnityModManager.ModEntry LogDebug(() => "Loading compat with PersistentJobs"); PersJobsJobTrackChangedEventRegMethod = AccessTools.Method(AccessTools.TypeByName("PersistentJobsMod.ModInteraction.PersistentJobsModInteractionFeatures"), "RegisterJobTracksChangedListener", [typeof(Action)]); + PersJobsJobTrackChangedEventUnregMethod = AccessTools.Method(AccessTools.TypeByName("PersistentJobsMod.ModInteraction.PersistentJobsModInteractionFeatures"), "UnregisterJobTracksChangedListener", [typeof(Action)]); PersJobsJobCarChangedEventRegMethod = AccessTools.Method(AccessTools.TypeByName("PersistentJobsMod.ModInteraction.PersistentJobsModInteractionFeatures"), "RegisterJobCarsChangedListener", [typeof(Action<(Job, Car)>)]); + PersJobsJobCarChangedEventUnregMethod = AccessTools.Method(AccessTools.TypeByName("PersistentJobsMod.ModInteraction.PersistentJobsModInteractionFeatures"), "UnegisterJobCarsChangedListener", [typeof(Action<(Job, Car)>)]); + PersJobsResumeCoroRunningField = AccessTools.Field(AccessTools.TypeByName("PersistentJobsMod.Optimization.FarCarOpt"), "ResumeCoroRunning"); PersJobs = true; } catch diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 96fd43eb..4ff7d720 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -48,6 +48,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Unity.Jobs; using UnityEngine; @@ -1199,10 +1200,11 @@ private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) { - if (NetworkLifecycle.Instance.IsHost()) - return; + if (NetworkLifecycle.Instance.IsHost()) return; + + LogDebug(() => $"Recieved ClientboundJobsUpdatePacket for station with id {packet.StationNetId} with {packet.JobUpdates.Length} potential updates"); - ClientboundJobUpdateQueue.Enqueue(packet); + ClientboundJobUpdateQueue.Enqueue(packet.Clone()); if (!jobUpdateProcessing) { jobUpdateProcessing = true; @@ -1223,7 +1225,7 @@ private IEnumerator ProcessClientboundJobUpdatePackets() continue; } - Log($"Received {packet.JobUpdates.Length} job updates for station {networkedStationController.StationController.logicStation.ID}"); + Log($"Processing {packet.JobUpdates.Length} job updates for station {networkedStationController.StationController.logicStation.ID}"); int waitingNSCsFrameCounter = 0; @@ -1244,10 +1246,11 @@ private IEnumerator ProcessClientboundJobUpdatePackets() private void OnClientboundTaskUpdatePacket(ClientboundTaskUpdatePacket packet) { - if (NetworkLifecycle.Instance.IsHost()) - return; + if (NetworkLifecycle.Instance.IsHost()) return; - ClientboundTaskUpdateQueue.Enqueue(packet); + LogDebug(() => $"Recieved ClientboundTaskUpdatePacket for task with id {packet.TaskNetId} of job with id {packet.JobNetId}"); + + ClientboundTaskUpdateQueue.Enqueue(packet.Clone()); if (!taskUpdateProcessing) { taskUpdateProcessing = true; @@ -1261,28 +1264,19 @@ private IEnumerator ProcessClientboundTaskUpdatePackets() { while (ClientboundTaskUpdateQueue.Count > 0) { - int waitingNSCsFrameCounter = 0; - - if (NetworkedStationController.WaitingNSCs.Any()) LogDebug(() => $"Stations are waiting for new jobs, task updates will be processed later"); - while (waitingNSCsFrameCounter < 600 && NetworkedStationController.WaitingNSCs.Any()) - { - waitingNSCsFrameCounter++; - yield return null; - } - - if (waitingNSCsFrameCounter >= 600) LogWarning("NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for waitingNSCs"); - var packet = ClientboundTaskUpdateQueue.Dequeue(); + LogDebug(() => $"Processing ClientboundTaskUpdatePacket for task with id {packet.TaskNetId} of job with id {packet.JobNetId}, {ClientboundTaskUpdateQueue.Count} other updates still in queue"); - int delayedJobsWaitFrameCounter = 0; - if (NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) Multiplayer.LogDebug(() => $"Job with netId {packet.JobNetId} is delayed in its creation, will wait with task updating"); - while (delayedJobsWaitFrameCounter < 600 && NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) - { - delayedJobsWaitFrameCounter++; - yield return null; - } - - if (delayedJobsWaitFrameCounter >= 600) LogWarning($"NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for delayed job with netId {packet.JobNetId}"); + var st = Stopwatch.StartNew(); + if (NetworkedStationController.WaitingNSCs.Any()) LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Stations are waiting for new jobs, task updates will be processed later"); + yield return new WaitUntil(() => !NetworkedStationController.WaitingNSCs.Any() || st.Elapsed.Seconds >= 10); + if (st.Elapsed.Seconds >= 10) LogWarning("NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for waitingNSCs"); + yield return null; + if (NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) Multiplayer.LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Job with netId {packet.JobNetId} is delayed in its creation, will wait with task updating"); + yield return new WaitUntil(() => !NetworkedStationController.DelayedJobs.Contains(packet.JobNetId) || st.Elapsed.Seconds >= 20); + if (st.Elapsed.Seconds >= 20) LogWarning($"NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for delayed job with netId {packet.JobNetId}"); + yield return null; + st.Reset(); if (!NetworkedTask.TryGet(packet.TaskNetId, out Task task) || task == null) { @@ -1295,41 +1289,49 @@ private IEnumerator ProcessClientboundTaskUpdatePackets() task.SetState(packet.NewState); task.taskStartTime = packet.TaskStartTime; task.taskFinishTime = packet.TaskFinishTime; + LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Successful task state update for task with id {packet.TaskNetId}"); } if (packet.ReplaceDestTrack == true) { var track = (NetworkedRailTrack.TryGet(packet.DestTrackId, out RailTrack railTrack) ? railTrack.LogicTrack() : null); if (track != null) Traverse.Create(task).Field("destinationTrack").SetValue(track); + LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Successful destination track update for task with id {packet.TaskNetId}"); } if (packet.ReplaceCar == true) { + LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Car update for task with id {packet.TaskNetId} started"); + int carWaitCounter = 0; + if (!NetworkedTrainCar.TryGet(packet.CarNetID, out Car _)) LogWarning($"ProcessClientboundTaskUpdatePackets() car with netId {packet.CarNetID} not found yet, waiting!"); while (carWaitCounter < 600 && !NetworkedTrainCar.TryGet(packet.CarNetID, out Car _)) { carWaitCounter++; yield return null; } + if (carWaitCounter >= 600) LogWarning($"ProcessClientboundTaskUpdatePackets() timed out waiting for car with netId {packet.CarNetID}!"); if (!NetworkedTrainCar.TryGet(packet.CarNetID, out Car car)) { LogError($"No car for netId {packet.CarNetID} found, skipping replacement!"); - continue; } - - void ReplaceCar(Task t) + else { - var cars = Traverse.Create(t).Field("cars").GetValue>(); - if (cars == null) return; - if (cars.Remove(cars.FirstOrDefault(c => c.ID == car.ID))) + var cars = Traverse.Create(task).Field("cars").GetValue>(); + if (cars != null && cars.Remove(cars.FirstOrDefault(c => c.ID == car.ID))) { - cars.Add(car); - SingletonBehaviour.Instance.Run(NetworkedStationController.UpdateCarPlates([packet.CarNetID], t.Job.ID)); + var job = task.Job; + if (NetworkedJob.TryGetFromJob(job, out var netJob) && netJob.TryGetNetworkedStationControllerHandlingNetworkedJob(out var nsc)) + { + cars.Add(car); + nsc.UpdateCarPlates([packet.CarNetID], job.ID); + LogDebug(() => $"ProcessClientboundTaskUpdatePackets() successfully updated car reference {car.ID} in task with netId {packet.TaskNetId} for job {task.Job.ID}"); + } + else LogError($"Why..., just WHY ?!?"); } + else LogWarning($"{task.GetType().Name} with netId {packet.TaskNetId} for job {task.Job.ID} doesn´t have cars!"); } - - NetworkedTask.DoOnActualTask(task, ReplaceCar); } yield return WaitFor.EndOfFrame; @@ -1919,7 +1921,7 @@ public void SendWarehouseRequest(WarehouseAction action, ushort netId) }, DeliveryMethod.ReliableUnordered); } - public void SendJobsRequest(NetworkedStationController station, ushort[] alreadyPresent, bool generateJobs) + public void SendJobsRequest(NetworkedStationController station, ushort[] alreadyPresent, bool generateJobs, ushort[] specificJobs) { LogDebug(() => $"Client asking for jobs in {station.StationController.stationInfo.YardID}, excluding {alreadyPresent.Length} already present jobs "); noCarTimeout = true; @@ -1929,6 +1931,7 @@ public void SendJobsRequest(NetworkedStationController station, ushort[] already { StationNetId = station.NetId, GenerateJobs = generateJobs, + SpecificJobsNetIds = specificJobs, ExcludeJobNetId = alreadyPresent }, DeliveryMethod.ReliableUnordered); } diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index a6703cbe..d5b8fa4e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -963,7 +963,8 @@ public void SendJobsUpdatePacket(uint stationNetId, NetworkedJob[] jobs) public void SendTaskUpdate(ushort jobNetId, ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) { - Multiplayer.Log($"Sending TaskUpdate for taskNetId {taskNetId}, newState {newState}"); + NetworkedJob.TryGetJob(jobNetId, out var job); + Multiplayer.Log($"Sending TaskUpdate for taskNetId {taskNetId}, in job {job.ID} newState {newState}"); SendPacketToAll ( new ClientboundTaskUpdatePacket @@ -2044,21 +2045,42 @@ private void OnServerboundJobsRequestPacket(ServerboundJobsRequestPacket packet, return; } + SingletonBehaviour.Instance.Run(SendJobsToClientsOnRequest(packet, networkedStationController)); + } + + private IEnumerator SendJobsToClientsOnRequest(ServerboundJobsRequestPacket packet, NetworkedStationController networkedStationController) + { + if (Multiplayer.PersJobs) + { + if ((bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null)) LogDebug(() => $"ServerboundJobsRequestPacket station {packet.StationNetId} is probably resuming cars, waiting"); + while ((bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null)) yield return null; + } + if (packet.GenerateJobs) { LogDebug(() => $"ServerboundJobsRequestPacket requested job generation for station {packet.StationNetId}"); - networkedStationController.StationController.ProceduralJobsController.TryToGenerateJobs(); + + if (!networkedStationController.StationController.ProceduralJobsController.IsJobGenerationActive) + { + networkedStationController.StationController.ProceduralJobsController.TryToGenerateJobs(); + yield return null; + } + else + { + LogDebug(() => $"ServerboundJobsRequestPacket station {packet.StationNetId} is already generating, won´t trigger again"); + } } - SingletonBehaviour.Instance.Run(SendJobsToClientsOnRequest(packet, networkedStationController)); - } + if (Multiplayer.PersJobs) + { + if ((bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null)) LogDebug(() => $"ServerboundJobsRequestPacket station {packet.StationNetId} is probably resuming cars, waiting"); + while ((bool)Multiplayer.PersJobsResumeCoroRunningField.GetValue(null)) yield return null; + } - private IEnumerator SendJobsToClientsOnRequest(ServerboundJobsRequestPacket packet, NetworkedStationController networkedStationController) - { if (networkedStationController.StationController.ProceduralJobsController.IsJobGenerationActive) LogDebug(() => $"Station {networkedStationController.StationController.stationInfo.YardID} is still generating jobs, will wait with sending"); while (networkedStationController.StationController.ProceduralJobsController.IsJobGenerationActive) yield return null; - var send = networkedStationController.NetworkedJobs.Where(nj => !packet.ExcludeJobNetId.Contains(nj.NetId)).ToArray(); + NetworkedJob[] send = (packet.SpecificJobsNetIds.Any()) ? networkedStationController.NetworkedJobs.Where(nj => packet.SpecificJobsNetIds.Contains(nj.NetId)).ToArray() : networkedStationController.NetworkedJobs.Where(nj => !packet.ExcludeJobNetId.Contains(nj.NetId)).ToArray(); NetworkLifecycle.Instance.Server.SendJobsCreatePacket(networkedStationController, send); } diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs index cf371176..32d1da3a 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs @@ -10,7 +10,8 @@ public class ClientboundJobsUpdatePacket public uint StationNetId { get; set; } public JobUpdateStruct[] JobUpdates { get; set; } - + public ClientboundJobsUpdatePacket Clone() => new() { StationNetId = StationNetId, JobUpdates = JobUpdates }; + public static ClientboundJobsUpdatePacket FromNetworkedJobs(uint stationNetID, NetworkedJob[] jobs) { Multiplayer.Log($"ClientboundJobsUpdatePacket.FromNetworkedJobs({stationNetID}, {jobs.Length})"); diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs index 560a090f..3c2750aa 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs @@ -2,7 +2,7 @@ namespace Multiplayer.Networking.Packets.Clientbound.Jobs; -internal class ClientboundTaskUpdatePacket +public class ClientboundTaskUpdatePacket { public ushort JobNetId { get; set; } public ushort TaskNetId { get; set; } @@ -14,4 +14,6 @@ internal class ClientboundTaskUpdatePacket public bool ReplaceCar { get; set; } public ushort DestTrackId { get; set; } public bool ReplaceDestTrack { get; set; } + + public ClientboundTaskUpdatePacket Clone() => new() { TaskNetId = this.TaskNetId, JobNetId = this.JobNetId, TaskStateUpdate = this.TaskStateUpdate, NewState = this.NewState, TaskStartTime = this.TaskStartTime, TaskFinishTime = this.TaskFinishTime, ReplaceDestTrack = this.ReplaceDestTrack, DestTrackId = this.DestTrackId, ReplaceCar = this.ReplaceCar, CarNetID = this.CarNetID }; } diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs index 66c6496d..65852a6c 100644 --- a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobsRequestPacket.cs @@ -6,5 +6,6 @@ public class ServerboundJobsRequestPacket { public uint StationNetId { get; set; } public bool GenerateJobs { get; set; } + public ushort[] SpecificJobsNetIds { get; set; } public ushort[] ExcludeJobNetId { get; set; } } diff --git a/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs index 112ac4a1..f7848ab2 100644 --- a/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs @@ -23,7 +23,9 @@ private static bool Prefix(StationProceduralJobsController __instance) } else { - networkedStationController.AskServerForAdditionalJobs(true); + bool generate = !networkedStationController.NetworkedJobs.Any(); + if (generate) Multiplayer.LogDebug(() => $"StationProceduralJobsController_TryToGenerateJobs_Patch: Station {__instance.stationController.stationInfo.YardID} requesting job generation on server"); + networkedStationController.AskServerForAdditionalJobs(generate); } } return NetworkLifecycle.Instance.IsHost(); diff --git a/Multiplayer/Utils/CombinedTypes.cs b/Multiplayer/Utils/CombinedTypes.cs new file mode 100644 index 00000000..36002ebf --- /dev/null +++ b/Multiplayer/Utils/CombinedTypes.cs @@ -0,0 +1,87 @@ +using Humanizer; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Multiplayer.Utils +{ +#nullable enable + public class HashSetQueue + { + private readonly Queue queue = new(); + private readonly HashSet set = new(); + + public int Count => set.Count; + + public bool Enqueue(T item) + { + if (!set.Add(item)) + return false; + + queue.Enqueue(item); + return true; + } + + public T Dequeue() + { + T item = queue.Dequeue(); + set.Remove(item); + return item; + } + + public bool TryDequeue(out T item) + { + if (set.Count == 0) + { + item = default!; + return false; + } + + item = queue.Dequeue(); + set.Remove(item); + return true; + } + + public bool Contains(T item) => set.Contains(item); + + public bool Any() => set.Any(); + + public bool Any(System.Func predicate) => set.Any(predicate); + + public void Clear() + { + queue.Clear(); + set.Clear(); + } + } + + public readonly struct CarPlateUpdate(IEnumerable carIds, string jobId) : IEquatable + { + public IReadOnlyCollection CarIds { get; } = carIds.ToArray(); + public string JobId { get; } = jobId; + + public bool Equals(CarPlateUpdate other) + { + return JobId == other.JobId && CarIds.Count == other.CarIds.Count && new HashSet(CarIds).SetEquals(other.CarIds); + } + + public override bool Equals(object? obj) => ((obj is CarPlateUpdate other) && Equals(other)); + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 31 + (JobId?.GetHashCode() ?? 0); + foreach (ushort id in CarIds) hash = (hash * 31) + id.GetHashCode(); + return hash; + } + } + + public void Deconstruct(out IReadOnlyCollection carIds, out string jobId) + { + carIds = CarIds; + jobId = JobId; + } + } +} From bc05977d7a0759ccdc8596fb77a6b742585bb7b8 Mon Sep 17 00:00:00 2001 From: T3M8029 <102697658+T3M8029@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:51:19 +0200 Subject: [PATCH 3/4] primitive fix for tc state desync after spawn --- .../Networking/Jobs/NetworkedJob.cs | 2 +- .../Train/NetworkTrainsetWatcher.cs | 1 + .../Networking/Train/NetworkedTrainCar.cs | 32 +++++++++++++---- Multiplayer/Patches/Train/CarSpawnerPatch.cs | 35 ++++++++++++++++++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 1a7b891d..2dbb78f7 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -314,7 +314,7 @@ private IEnumerator OnJobCarChangedDelayed((Job, Car) jct) } yield return null; var (job, car) = jct; - if (job.ID == Job.ID) foreach (var task in job.tasks) NetworkedTask.DoOnActualTask(task, t => { if (NetworkedTask.TryGet(t, out var netTask)) netTask.UpdateCar(car); }); + if (job.ID == Job.ID) foreach (var task in job.tasks) NetworkedTask.DoOnActualTask(task, t => { if (((t.GetType() != typeof(ParallelTasks)) && (t.GetType() != typeof(SequentialTasks))) && (NetworkedTask.TryGet(t, out var netTask))) netTask.UpdateCar(car); }); yield break; } diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 364a3cb4..ee444c17 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -104,6 +104,7 @@ private void Server_TickSet(Trainset set, uint tick) // If we can locate the networked car, we'll add to the ticks counter and check if any tracks are dirty if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC) && netTC != null) { + if (netTC.doNotUpdate) return; maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; //Even if the car is stationary, if the max ticks has been exceeded we will still sync anyTracksDirty |= netTC.BogieTracksDirty; } diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 4d6d574c..27cd93a3 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -210,6 +210,9 @@ static string GetFuse(uint netId) private readonly Dictionary portAuthority = []; + public bool doNotUpdate = true; + public uint? startTick = null; + #endregion #region Client Variables @@ -850,14 +853,29 @@ private void Server_OnTick(uint tick) if (UnloadWatcher.isUnloading) return; - Server_SendBrakeStates(); - Server_SendCouplers(); - Server_SendCables(); - Server_SendCargoState(); - Server_SendCargoHealthUpdate(); - Server_SendCarHealthState(); + if (!startTick.HasValue) + { + startTick = tick; + Server_SendCargoState(); + Server_SendCargoHealthUpdate(); + Server_SendCarHealthState(); + } + + if (!doNotUpdate) + { + Server_SendBrakeStates(); + Server_SendCouplers(); + Server_SendCables(); + Server_SendCargoState(); + Server_SendCargoHealthUpdate(); + Server_SendCarHealthState(); - TicksSinceSync++; //keep track of last full sync + TicksSinceSync++; //keep track of last full sync + } + else + { + if ((tick - startTick > 120) && !((bool)Multiplayer.PersJobsResumeCoroRunningField?.GetValue(null) == true)) doNotUpdate = false; + } } private void Server_SendBrakeStates() diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index 175cfecd..0a015628 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -1,14 +1,21 @@ +using DV.UIFramework; +using DV.Utils; using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; +using System.Collections; using System.Collections.Generic; +using UnityEngine; namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(CarSpawner))] public static class CarSpawner_Patch { + private static readonly HashSet carIdsWithNoUpdates = []; + private static bool allowingCrasUpdatesCoroRunning = false; + [HarmonyPatch(nameof(CarSpawner.PrepareTrainCarForDeleting))] [HarmonyPrefix] private static void PrepareTrainCarForDeleting(TrainCar trainCar) @@ -92,7 +99,33 @@ private static void SpawnLoadedCar(TrainCar __result) if (__result == null) return; + if (!WorldStreamingInit.IsLoaded) + return; + Multiplayer.LogDebug(() => $"SpawnLoadedCar() {__result?.carLivery?.name} spawned, sending to players"); - NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], false, true); + + if (__result.TryNetworked(out var netTC)) + { + netTC.doNotUpdate = true; + carIdsWithNoUpdates.Add(__result.ID); + NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], false, true); + + if (!allowingCrasUpdatesCoroRunning) + { + SingletonBehaviour.Instance.Run(AllowingCrasUpdatesCoro()); + } + } + } + + private static IEnumerator AllowingCrasUpdatesCoro() + { + yield return null; + + Multiplayer.LogDebug(() => $"CarSpawnerPatch: waiting with newly resumed car physics updates for all cars to resume"); + yield return new WaitUntil(() => !((bool)Multiplayer.PersJobsResumeCoroRunningField?.GetValue(null) == true)); + yield return WaitFor.SecondsRealtime(3f); + Multiplayer.LogDebug(() => $"CarSpawnerPatch: car resuming finished, will allow physics updates for cars {(string.Join(", ", carIdsWithNoUpdates))}"); + + foreach (var tcId in carIdsWithNoUpdates) if (NetworkedTrainCar.GetFromTrainId(tcId, out var ntc)) ntc.doNotUpdate = false; } } From 78f5d5ccde4c085f4d3b18a5c09e43d41eef1575 Mon Sep 17 00:00:00 2001 From: T3M8029 <102697658+T3M8029@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:12:59 +0200 Subject: [PATCH 4/4] Hanlde jobs on client via JobsManager (for RD compatibility) + additional leanup --- .../Networking/Jobs/NetworkedJob.cs | 2 +- .../Networking/Jobs/NetworkedTask.cs | 2 +- .../World/NetworkedStationController.cs | 8 +++- Multiplayer/Networking/Data/Jobs/JobData.cs | 11 ++++- .../Managers/Client/NetworkClient.cs | 32 +++++++++------ Multiplayer/Patches/Train/CarSpawnerPatch.cs | 22 +++++++--- .../Patches/World/CarsSaveManagerPatch.cs | 33 --------------- Multiplayer/Utils/DvExtensions.cs | 41 +++++++++++++++++++ 8 files changed, 93 insertions(+), 58 deletions(-) diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs index 2dbb78f7..56b9feaa 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -406,7 +406,7 @@ protected void OnDisable() if (Multiplayer.PersJobs) { - Multiplayer.PersJobsJobTrackChangedEventRegMethod.Invoke(null, OnJobTrackChengeEventRegistrator); + Multiplayer.PersJobsJobTrackChangedEventUnregMethod.Invoke(null, OnJobTrackChengeEventRegistrator); Multiplayer.PersJobsJobCarChangedEventUnregMethod.Invoke(null, OnJobCarChangedEventRegistrator); OnJobTrackChengeEventRegistrator = null; OnJobCarChangedEventRegistrator = null; diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs index 213a0e86..f8a79ed7 100644 --- a/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs +++ b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs @@ -57,7 +57,7 @@ public static void DoOnActualTask(Task task, Action action) .GetValue>() .Do(t => DoOnActualTask(t, action)); } - action(task); + else action(task); } protected override bool IsIdServerAuthoritative => true; diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs index f13120f5..efa45a4a 100644 --- a/Multiplayer/Components/Networking/World/NetworkedStationController.cs +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -345,6 +345,8 @@ public void AddJobs(JobData[] jobs, bool noTimeout = false) if (NetworkedJobs.Any(nj => nj.Job.ID == jobData.ID)) { Multiplayer.LogWarning($"Client already has job {jobData.ID}, not adding second copy"); + DelayedJobs.Remove(jobData.NetID); + TimeoutJobs.Remove(jobData.NetID); continue; } @@ -567,6 +569,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat takenJobs.Add(netJob.Job); netJob.Job.TakeJob(true); //take job as if loaded from save to prevent debt controller kicking in + SingletonBehaviour.Instance.currentJobs.Add(netJob.Job); if (canPrint) { @@ -584,7 +587,7 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat case JobState.Completed: takenJobs.Remove(netJob.Job); completedJobs.Add(netJob.Job); - netJob.Job.CompleteJob(); + SingletonBehaviour.Instance.CompleteTheJob(netJob.Job); if (canPrint) { @@ -605,12 +608,13 @@ private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateDat case JobState.Abandoned: takenJobs.Remove(netJob.Job); abandonedJobs.Add(netJob.Job); - netJob.Job.AbandonJob(); + SingletonBehaviour.Instance.AbandonJob(netJob.Job); UpdateCarPlates(netJob.JobCars, string.Empty); break; case JobState.Expired: netJob.Job.ExpireJob(); + SingletonBehaviour.Instance.currentJobs.Remove(netJob.Job); netJob.DestroyJobOverview(); UpdateCarPlates(netJob.JobCars, string.Empty); diff --git a/Multiplayer/Networking/Data/Jobs/JobData.cs b/Multiplayer/Networking/Data/Jobs/JobData.cs index 05632a8c..634c2703 100644 --- a/Multiplayer/Networking/Data/Jobs/JobData.cs +++ b/Multiplayer/Networking/Data/Jobs/JobData.cs @@ -1,13 +1,16 @@ using DV.Logic.Job; using DV.ThingTypes; +using DV.Utils; +using HarmonyLib; using LiteNetLib.Utils; using MPAPI.Types; using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data.Items; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System; -using Multiplayer.Networking.Data.Items; namespace Multiplayer.Networking.Data.Jobs; @@ -94,6 +97,10 @@ public static (Job newJob, Dictionary netIdToTask) ToJob(JobData j State = jobData.State, }; + HashSet cars = []; + jobData.GetCars().Do(cid => { if (NetworkedTrainCar.TryGet(cid, out Car car)) cars.Add(car); }); + SingletonBehaviour.Instance.RegisterGeneratedJob(newJob, cars); + return new(newJob, netIdToTask); } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index 4ff7d720..9b91da45 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1267,16 +1267,19 @@ private IEnumerator ProcessClientboundTaskUpdatePackets() var packet = ClientboundTaskUpdateQueue.Dequeue(); LogDebug(() => $"Processing ClientboundTaskUpdatePacket for task with id {packet.TaskNetId} of job with id {packet.JobNetId}, {ClientboundTaskUpdateQueue.Count} other updates still in queue"); - var st = Stopwatch.StartNew(); - if (NetworkedStationController.WaitingNSCs.Any()) LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Stations are waiting for new jobs, task updates will be processed later"); - yield return new WaitUntil(() => !NetworkedStationController.WaitingNSCs.Any() || st.Elapsed.Seconds >= 10); - if (st.Elapsed.Seconds >= 10) LogWarning("NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for waitingNSCs"); - yield return null; - if (NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) Multiplayer.LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Job with netId {packet.JobNetId} is delayed in its creation, will wait with task updating"); - yield return new WaitUntil(() => !NetworkedStationController.DelayedJobs.Contains(packet.JobNetId) || st.Elapsed.Seconds >= 20); - if (st.Elapsed.Seconds >= 20) LogWarning($"NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for delayed job with netId {packet.JobNetId}"); - yield return null; - st.Reset(); + if ((NetworkedStationController.WaitingNSCs.Any()) || (NetworkedStationController.DelayedJobs.Contains(packet.JobNetId))) + { + var st = Stopwatch.StartNew(); + if (NetworkedStationController.WaitingNSCs.Any()) LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Stations are waiting for new jobs, task updates will be processed later"); + yield return new WaitUntil(() => !NetworkedStationController.WaitingNSCs.Any() || st.Elapsed.Seconds >= 10); + if (st.Elapsed.Seconds >= 10) LogWarning("NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for waitingNSCs"); + yield return null; + if (NetworkedStationController.DelayedJobs.Contains(packet.JobNetId)) Multiplayer.LogDebug(() => $"NetworkClient.ProcessClientboundTaskUpdatePackets: Job with netId {packet.JobNetId} is delayed in its creation, will wait with task updating"); + yield return new WaitUntil(() => !NetworkedStationController.DelayedJobs.Contains(packet.JobNetId) || st.Elapsed.Seconds >= 20); + if (st.Elapsed.Seconds >= 20) LogWarning($"NetworkClient.ProcessClientboundTaskUpdatePackets timed out waiting for delayed job with netId {packet.JobNetId}"); + yield return null; + st.Reset(); + } if (!NetworkedTask.TryGet(packet.TaskNetId, out Task task) || task == null) { @@ -1319,18 +1322,21 @@ private IEnumerator ProcessClientboundTaskUpdatePackets() else { var cars = Traverse.Create(task).Field("cars").GetValue>(); - if (cars != null && cars.Remove(cars.FirstOrDefault(c => c.ID == car.ID))) + Car carToReplace = cars.FirstOrDefault(c => c.ID == car.ID); + if (cars != null && cars.Remove(carToReplace)) { var job = task.Job; + var jobToCarsDict = SingletonBehaviour.Instance.jobToJobCars; + if (jobToCarsDict.TryGetValue(job, out var dictCars)) jobToCarsDict[job] = (dictCars?.Replace(carToReplace, car).ToHashSet()); if (NetworkedJob.TryGetFromJob(job, out var netJob) && netJob.TryGetNetworkedStationControllerHandlingNetworkedJob(out var nsc)) { cars.Add(car); nsc.UpdateCarPlates([packet.CarNetID], job.ID); - LogDebug(() => $"ProcessClientboundTaskUpdatePackets() successfully updated car reference {car.ID} in task with netId {packet.TaskNetId} for job {task.Job.ID}"); + LogDebug(() => $"ProcessClientboundTaskUpdatePackets() successfully updated car reference {car.ID} in {task.GetType().Name} with netId {packet.TaskNetId} for job {task.Job.ID}"); } else LogError($"Why..., just WHY ?!?"); } - else LogWarning($"{task.GetType().Name} with netId {packet.TaskNetId} for job {task.Job.ID} doesn´t have cars!"); + else LogWarning($"{task.GetType().Name} with netId {packet.TaskNetId} for job {task.Job.ID} doesn´t have relevant cars!"); } } diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index 0a015628..76c5861f 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -106,6 +106,7 @@ private static void SpawnLoadedCar(TrainCar __result) if (__result.TryNetworked(out var netTC)) { + TrainStress.globalIgnoreStressCalculation = true; netTC.doNotUpdate = true; carIdsWithNoUpdates.Add(__result.ID); NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], false, true); @@ -119,13 +120,22 @@ private static void SpawnLoadedCar(TrainCar __result) private static IEnumerator AllowingCrasUpdatesCoro() { - yield return null; + try + { + allowingCrasUpdatesCoroRunning = true; + yield return null; - Multiplayer.LogDebug(() => $"CarSpawnerPatch: waiting with newly resumed car physics updates for all cars to resume"); - yield return new WaitUntil(() => !((bool)Multiplayer.PersJobsResumeCoroRunningField?.GetValue(null) == true)); - yield return WaitFor.SecondsRealtime(3f); - Multiplayer.LogDebug(() => $"CarSpawnerPatch: car resuming finished, will allow physics updates for cars {(string.Join(", ", carIdsWithNoUpdates))}"); + Multiplayer.LogDebug(() => $"CarSpawnerPatch: waiting with newly resumed car physics updates for all cars to resume"); + yield return new WaitUntil(() => !((bool)Multiplayer.PersJobsResumeCoroRunningField?.GetValue(null) == true)); + yield return WaitFor.SecondsRealtime(3f); + Multiplayer.LogDebug(() => $"CarSpawnerPatch: car resuming finished, will allow physics updates for cars {(string.Join(", ", carIdsWithNoUpdates))}"); - foreach (var tcId in carIdsWithNoUpdates) if (NetworkedTrainCar.GetFromTrainId(tcId, out var ntc)) ntc.doNotUpdate = false; + foreach (var tcId in carIdsWithNoUpdates) if (NetworkedTrainCar.GetFromTrainId(tcId, out var ntc)) ntc.doNotUpdate = false; + } + finally + { + TrainStress.globalIgnoreStressCalculation = false; + allowingCrasUpdatesCoroRunning = false; + } } } diff --git a/Multiplayer/Patches/World/CarsSaveManagerPatch.cs b/Multiplayer/Patches/World/CarsSaveManagerPatch.cs index 8d1bffc4..43a4b284 100644 --- a/Multiplayer/Patches/World/CarsSaveManagerPatch.cs +++ b/Multiplayer/Patches/World/CarsSaveManagerPatch.cs @@ -32,38 +32,5 @@ private static void RestoreCarConnections_Postfix(JObject carData) { NetworkLifecycle.Instance.Server.SendAbsoluteCouplingStatus(trainCarByCarGuid); } - //SingletonBehaviour.Instance.Run(SendConnectionsForRestoredCarDelayed(carData)); - } - - private static IEnumerator SendConnectionsForRestoredCarDelayed(JObject carData) - { - yield return WaitFor.SecondsRealtime(0.25f); - - TrainCar trainCarByCarGuid = SingletonBehaviour.Instance.GetTrainCarByCarGuid(carData.GetString("carGuid")); - if (trainCarByCarGuid != null && NetworkedTrainCar.TryGetFromTrainCar(trainCarByCarGuid, out var networkedTrainCar)) - { - NetworkLifecycle.Instance.Server.SendAbsoluteCouplingStatus(trainCarByCarGuid); - - /*var frontOtherCoupler = trainCarByCarGuid.frontCoupler.GetAirHoseConnectedTo(); - if (frontOtherCoupler != null && (!(carData.GetBool("airHoseF") == true) && !(carData.GetBool("airCockF") == true))) - { - frontOtherCoupler.IsCockOpen = false; - frontOtherCoupler.DisconnectAirHose(false); - } - - var rearOtherCoupler = trainCarByCarGuid.frontCoupler.GetAirHoseConnectedTo(); - if (rearOtherCoupler != null && (!(carData.GetBool("airHoseR") == true) && !(carData.GetBool("airCockR") == true))) - { - rearOtherCoupler.IsCockOpen = false; - rearOtherCoupler.DisconnectAirHose(false); - } - - yield return null; - - networkedTrainCar.sendCouplers = true; - networkedTrainCar.sendCables = true; - networkedTrainCar.Server_SendCouplers(); - networkedTrainCar.Server_SendCables();*/ - } } } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 8c496d60..b2a1c726 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -7,6 +7,9 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityEngine.UI; @@ -174,4 +177,42 @@ public static bool AllowPause() (NetworkLifecycle.Instance.Server.PlayerCount == 1 && NetworkLifecycle.Instance.IsClientRunning)); } #endregion + + #region GenericExtensions + + public static int Replace(this IList source, T oldValue, T newValue) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var index = source.IndexOf(oldValue); + if (index != -1) + source[index] = newValue; + return index; + } + + public static void ReplaceAll(this IList source, T oldValue, T newValue) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + int index = -1; + do + { + index = source.IndexOf(oldValue); + if (index != -1) + source[index] = newValue; + } while (index != -1); + } + + + public static IEnumerable Replace(this IEnumerable source, T oldValue, T newValue) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + return source.Select(x => EqualityComparer.Default.Equals(x, oldValue) ? newValue : x); + } + + #endregion }