From e0ee7cdaa791927a1ef26f336439f99e7c2bd824 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Wed, 21 Oct 2026 18:22:13 +0500 Subject: [PATCH] Fix stale player-state subscriptions after scene changes --- .../Runtime/App/CollectionSceneController.cs | 5 ++++ .../Runtime/App/DeckBuilderSceneController.cs | 5 ++++ .../Runtime/App/LobbySceneController.cs | 6 ++++ .../Vanguard/Runtime/UI/CollectionView.cs | 21 ++++++++++++- .../Assets/Vanguard/Runtime/UI/DecksView.cs | 21 ++++++++++++- .../Assets/Vanguard/Runtime/UI/LoginView.cs | 7 +++++ client/Assets/Vanguard/Runtime/UI/MetaView.cs | 26 ++++++++++++++-- .../Assets/Vanguard/Runtime/UI/StoreView.cs | 30 ++++++++++++++++++- .../Vanguard/Tests/Editor/ClientSmokeTests.cs | 14 +++++++++ 9 files changed, 130 insertions(+), 5 deletions(-) diff --git a/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs b/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs index 0472bf6..41fb8ab 100644 --- a/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs +++ b/client/Assets/Vanguard/Runtime/App/CollectionSceneController.cs @@ -19,5 +19,10 @@ namespace Vanguard.App Resolve(), () => _ = Flow.LoadLobbyAsync()); } + + private void OnDestroy() + { + _view?.Dispose(); + } } } diff --git a/client/Assets/Vanguard/Runtime/App/DeckBuilderSceneController.cs b/client/Assets/Vanguard/Runtime/App/DeckBuilderSceneController.cs index 321df07..7b9f65e 100644 --- a/client/Assets/Vanguard/Runtime/App/DeckBuilderSceneController.cs +++ b/client/Assets/Vanguard/Runtime/App/DeckBuilderSceneController.cs @@ -21,5 +21,10 @@ namespace Vanguard.App () => _ = Flow.LoadLobbyAsync(), deck => _ = Flow.OpenLobbyWithQueueAsync(deck)); } + + private void OnDestroy() + { + _view?.Dispose(); + } } } diff --git a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs index 3491516..2af780e 100644 --- a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs +++ b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs @@ -111,5 +111,11 @@ namespace Vanguard.App _queue.Hide(); _store.Hide(); } + + private void OnDestroy() + { + _metaView?.Dispose(); + _store?.Dispose(); + } } } diff --git a/client/Assets/Vanguard/Runtime/UI/CollectionView.cs b/client/Assets/Vanguard/Runtime/UI/CollectionView.cs index 26f157d..1220632 100644 --- a/client/Assets/Vanguard/Runtime/UI/CollectionView.cs +++ b/client/Assets/Vanguard/Runtime/UI/CollectionView.cs @@ -12,11 +12,12 @@ using Vanguard.State; namespace Vanguard.UI { /// Коллекция игрока и разбор карт в пыль. - public sealed class CollectionView + public sealed class CollectionView : IDisposable { private readonly MetaService _meta; private readonly PlayerState _player; private readonly Action _onBack; + private bool _disposed; private readonly RectTransform _root; private readonly Text _balances; @@ -106,6 +107,15 @@ namespace Vanguard.UI _confirm.gameObject.SetActive(false); } + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _player.Changed -= OnPlayerChanged; + } + private RectTransform BuildConfirm(Transform parent) { var overlay = UiKit.CreatePanel(parent, "DismantleConfirm", new Color(0f, 0f, 0f, 0.72f)); @@ -145,6 +155,9 @@ namespace Vanguard.UI try { var collection = await _meta.GetCollectionAsync(); + if (_disposed) + return; + Rebuild(collection.Items ?? new List()); SetStatus( collection.Items == null || collection.Items.Count == 0 @@ -286,12 +299,18 @@ namespace Vanguard.UI private void OnPlayerChanged() { + if (_disposed || _balances == null) + return; + _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust + " · Токены " + _player.Tokens; } private void SetStatus(string text, Color color) { + if (_disposed || _status == null) + return; + _status.text = text ?? string.Empty; _status.color = color; } diff --git a/client/Assets/Vanguard/Runtime/UI/DecksView.cs b/client/Assets/Vanguard/Runtime/UI/DecksView.cs index 1e0405a..414c22b 100644 --- a/client/Assets/Vanguard/Runtime/UI/DecksView.cs +++ b/client/Assets/Vanguard/Runtime/UI/DecksView.cs @@ -12,12 +12,13 @@ using Vanguard.State; namespace Vanguard.UI { /// Список колод и быстрый сборщик стартовой колоды из триал-деки. - public sealed class DecksView + public sealed class DecksView : IDisposable { private readonly MetaService _meta; private readonly PlayerState _player; private readonly Action _onBack; private readonly Action _onPlay; + private bool _disposed; private readonly RectTransform _root; private RectTransform _content; @@ -122,6 +123,15 @@ namespace Vanguard.UI _confirm.gameObject.SetActive(false); } + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _player.Changed -= OnPlayerChanged; + } + private RectTransform BuildConfirm(Transform parent) { var overlay = UiKit.CreatePanel(parent, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.72f)); @@ -161,6 +171,9 @@ namespace Vanguard.UI try { var decks = await _meta.GetDecksAsync(); + if (_disposed) + return; + Rebuild(decks); SetStatus( decks.Count == 0 ? "Колод пока нет." : "Колод: " + decks.Count + ".", @@ -324,12 +337,18 @@ namespace Vanguard.UI private void OnPlayerChanged() { + if (_disposed || _balances == null) + return; + _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust + " · Токены " + _player.Tokens; } private void SetStatus(string text, Color color) { + if (_disposed || _status == null) + return; + _status.text = text ?? string.Empty; _status.color = color; } diff --git a/client/Assets/Vanguard/Runtime/UI/LoginView.cs b/client/Assets/Vanguard/Runtime/UI/LoginView.cs index af0b7a3..5c2b620 100644 --- a/client/Assets/Vanguard/Runtime/UI/LoginView.cs +++ b/client/Assets/Vanguard/Runtime/UI/LoginView.cs @@ -229,6 +229,10 @@ namespace Vanguard.UI private void SetBusy(bool busy, string status) { + if (_loginButton == null || _registerButton == null || _serverUrl == null + || _userName == null || _password == null) + return; + _busy = busy; _loginButton.interactable = !busy; _registerButton.interactable = !busy; @@ -242,6 +246,9 @@ namespace Vanguard.UI private void SetStatus(string text, Color color) { + if (_status == null) + return; + _status.text = text ?? string.Empty; _status.color = color; } diff --git a/client/Assets/Vanguard/Runtime/UI/MetaView.cs b/client/Assets/Vanguard/Runtime/UI/MetaView.cs index 0107cf8..df0fd01 100644 --- a/client/Assets/Vanguard/Runtime/UI/MetaView.cs +++ b/client/Assets/Vanguard/Runtime/UI/MetaView.cs @@ -9,7 +9,7 @@ using Vanguard.State; namespace Vanguard.UI { /// Главный экран после входа: профиль, балансы и заглушки разделов. - public sealed class MetaView + public sealed class MetaView : IDisposable { private readonly AuthService _auth; private readonly PlayerState _player; @@ -19,6 +19,7 @@ namespace Vanguard.UI private readonly Action _onStore; private readonly Action _onPlay; private readonly Action _onOnboarding; + private bool _disposed; private readonly RectTransform _root; private readonly Text _playerName; @@ -144,6 +145,15 @@ namespace Vanguard.UI public void Hide() => UiKit.SetActive(_root, false); + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _player.Changed -= UpdateTexts; + } + private static Text AddStat(Transform parent, string caption) { var card = UiKit.CreatePanel(parent, "Stat_" + caption, new Color(0.09f, 0.11f, 0.15f, 1f)); @@ -183,11 +193,15 @@ namespace Vanguard.UI try { await _auth.RefreshMeAsync(); + if (_disposed) + return; + SetStatus("Профиль обновлён.", new Color(0.45f, 0.85f, 0.55f)); } catch (ApiException exception) when (exception.StatusCode == 401) { - _onLoggedOut(); + if (!_disposed) + _onLoggedOut(); } catch (Exception exception) { @@ -202,6 +216,8 @@ namespace Vanguard.UI private void UpdateTexts() { + if (_disposed) + return; _playerName.text = string.IsNullOrEmpty(_player.UserName) ? "…" : _player.UserName; @@ -224,6 +240,9 @@ namespace Vanguard.UI private void SetStatus(string text, Color color) { + if (_disposed || _status == null) + return; + _status.text = text ?? string.Empty; _status.color = color; } @@ -232,6 +251,9 @@ namespace Vanguard.UI { _logout.interactable = false; await _auth.LogoutAsync(); + if (_disposed) + return; + _logout.interactable = true; _onLoggedOut(); } diff --git a/client/Assets/Vanguard/Runtime/UI/StoreView.cs b/client/Assets/Vanguard/Runtime/UI/StoreView.cs index 05a445b..580c71e 100644 --- a/client/Assets/Vanguard/Runtime/UI/StoreView.cs +++ b/client/Assets/Vanguard/Runtime/UI/StoreView.cs @@ -12,12 +12,13 @@ using Vanguard.State; namespace Vanguard.UI { /// Магазин в Lobby: бустеры и вторая триал-дека. - public sealed class StoreView + public sealed class StoreView : IDisposable { private readonly MetaService _meta; private readonly AuthService _auth; private readonly PlayerState _player; private readonly Action _onBack; + private bool _disposed; private readonly RectTransform _root; private readonly Text _balances; @@ -147,6 +148,15 @@ namespace Vanguard.UI UiKit.SetActive(_root, false); } + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _player.Changed -= OnPlayerChanged; + } + public void Tick() { } @@ -164,6 +174,9 @@ namespace Vanguard.UI { await _auth.EnsureAccessAsync(); _store = await _meta.GetStoreAsync(); + if (_disposed) + return; + RebuildTrials(_store.TrialDecks); _goldButton.GetComponentInChildren().text = "Купить за " + _store.BoosterPriceGold + " Gold"; @@ -319,6 +332,9 @@ namespace Vanguard.UI private async Task BuyBoosterAsync(string payment) { var result = await _meta.BuyBoosterAsync(payment); + if (_disposed) + return; + SetStatus("Бустер открыт!", new Color(0.45f, 0.85f, 0.55f)); ShowResult(result); } @@ -326,6 +342,9 @@ namespace Vanguard.UI private async Task BuyTrialAsync(StoreTrialDeckDto deck) { await _meta.BuyTrialDeckAsync(deck.Id); + if (_disposed) + return; + SetStatus("«" + deck.Name + "» добавлена в коллекцию.", new Color(0.45f, 0.85f, 0.55f)); await LoadAsync(); } @@ -437,6 +456,9 @@ namespace Vanguard.UI private void SetButtons(bool interactable) { + if (_disposed) + return; + if (_goldButton != null) _goldButton.interactable = interactable; if (_tokenButton != null) @@ -445,12 +467,18 @@ namespace Vanguard.UI private void OnPlayerChanged() { + if (_disposed || _balances == null) + return; + _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust + " · Токены " + _player.Tokens; } private void SetStatus(string text, Color color) { + if (_disposed || _status == null) + return; + _status.text = text ?? string.Empty; _status.color = color; } diff --git a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs index 98adad3..582b940 100644 --- a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs +++ b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using UnityEngine; using UnityEngine.EventSystems; using Vanguard.Api; +using Vanguard.Api.Contracts; using Vanguard.App; using Vanguard.Auth; using Vanguard.Meta; @@ -55,6 +56,19 @@ namespace Vanguard.Tests Assert.IsTrue(Exists("QueueScreen")); Assert.IsTrue(Exists("BattleScreen")); + // После смены сцены старые views не должны реагировать на PlayerState. + lobby.Dispose(); + collection.Dispose(); + decks.Dispose(); + store.Dispose(); + player.Apply(new MeResponse + { + UserName = "after-dispose", + Gold = 10, + Dust = 2, + Tokens = 1, + }); + Cleanup(); }