diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 4270291611..82d1ba2360 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -84,7 +84,14 @@ namespace OpenRA static void JoinInner(OrderManager om) { - OrderManager?.Dispose(); + // HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game. + // This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining + // a lobby, while keeping the OrderManager that runs the shellmap intact. + // A matching check in World.Dispose (which is called by WorldRenderer.Dispose) makes sure that we dispose + // the shellmap's OM when a lobby game actually starts. + if (OrderManager?.World == null || OrderManager.World.Type != WorldType.Shellmap) + OrderManager?.Dispose(); + OrderManager = om; } diff --git a/OpenRA.Game/Network/Connection.cs b/OpenRA.Game/Network/Connection.cs index 335ff1ff0b..b1571a9270 100644 --- a/OpenRA.Game/Network/Connection.cs +++ b/OpenRA.Game/Network/Connection.cs @@ -45,6 +45,7 @@ namespace OpenRA.Network readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new Queue<(int, int, ulong)>(); readonly Queue<(int Frame, OrderPacket Orders)> orders = new Queue<(int, OrderPacket)>(); readonly Queue immediateOrders = new Queue(); + bool disposed; int IConnection.LocalClientId => LocalClientId; @@ -72,8 +73,15 @@ namespace OpenRA.Network void IConnection.Receive(OrderManager orderManager) { while (immediateOrders.TryDequeue(out var i)) + { orderManager.ReceiveImmediateOrders(LocalClientId, i); + // An immediate order may trigger a chain of actions that disposes the OrderManager and connection. + // Bail out to avoid potential problems from acting on disposed objects. + if (disposed) + break; + } + // Project orders forward to the next frame while (orders.TryDequeue(out var o)) orderManager.ReceiveOrders(LocalClientId, (o.Frame + 1, o.Orders)); @@ -82,7 +90,10 @@ namespace OpenRA.Network orderManager.ReceiveSync(s); } - void IDisposable.Dispose() { } + void IDisposable.Dispose() + { + disposed = true; + } } public sealed class NetworkConnection : IConnection @@ -272,6 +283,11 @@ namespace OpenRA.Network { orderManager.ReceiveImmediateOrders(clientId, i); Recorder?.Receive(clientId, i.Serialize(0)); + + // An immediate order may trigger a chain of actions that disposes the OrderManager and connection. + // Bail out to avoid potential problems from acting on disposed objects. + if (disposed) + return; } while (sentSync.TryDequeue(out var s)) @@ -312,6 +328,11 @@ namespace OpenRA.Network if (record) Recorder?.Receive(p.FromClient, p.Data); + + // An immediate order may trigger a chain of actions that disposes the OrderManager and connection. + // Bail out to avoid potential problems from acting on disposed objects. + if (disposed) + return; } } diff --git a/OpenRA.Game/Network/OrderManager.cs b/OpenRA.Game/Network/OrderManager.cs index 274efa3841..5bddbf1363 100644 --- a/OpenRA.Game/Network/OrderManager.cs +++ b/OpenRA.Game/Network/OrderManager.cs @@ -128,19 +128,11 @@ namespace OpenRA.Network public void ReceiveDisconnect(int clientIndex) { - // HACK: The shellmap relies on ticking a disposed OM - if (disposed && World.Type != WorldType.Shellmap) - return; - pendingOrders.Remove(clientIndex); } public void ReceiveSync((int Frame, int SyncHash, ulong DefeatState) sync) { - // HACK: The shellmap relies on ticking a disposed OM - if (disposed && World.Type != WorldType.Shellmap) - return; - if (syncForFrame.TryGetValue(sync.Frame, out var s)) { if (s.SyncHash != sync.SyncHash || s.DefeatState != sync.DefeatState) @@ -152,10 +144,6 @@ namespace OpenRA.Network public void ReceiveImmediateOrders(int clientId, OrderPacket orders) { - // HACK: The shellmap relies on ticking a disposed OM - if (disposed && World.Type != WorldType.Shellmap) - return; - foreach (var o in orders.GetOrders(World)) { UnitOrders.ProcessOrder(this, World, clientId, o); @@ -168,10 +156,6 @@ namespace OpenRA.Network public void ReceiveOrders(int clientId, (int Frame, OrderPacket Orders) orders) { - // HACK: The shellmap relies on ticking a disposed OM - if (disposed && World.Type != WorldType.Shellmap) - return; - if (pendingOrders.TryGetValue(clientId, out var queue)) queue.Enqueue((orders.Frame, orders.Orders)); else diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index 3978b3f3d7..4e08bfd3a8 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -574,6 +574,12 @@ namespace OpenRA while (frameEndActions.Count != 0) frameEndActions.Dequeue()(this); + // HACK: The shellmap OrderManager is owned by its world in order to avoid + // problems with having multiple OMs active when joining a game lobby from the main menu. + // A matching check in Game.JoinInner handles OM disposal for all other cases. + if (Type == WorldType.Shellmap) + OrderManager.Dispose(); + Game.FinishBenchmark(); } }