Fix stale player-state subscriptions after scene changes
CI / test (push) Has been cancelled Details

This commit is contained in:
MpakobecEx 2026-10-21 18:22:13 +05:00
parent 797e45a527
commit e0ee7cdaa7
9 changed files with 130 additions and 5 deletions

View File

@ -19,5 +19,10 @@ namespace Vanguard.App
Resolve<PlayerState>(), Resolve<PlayerState>(),
() => _ = Flow.LoadLobbyAsync()); () => _ = Flow.LoadLobbyAsync());
} }
private void OnDestroy()
{
_view?.Dispose();
}
} }
} }

View File

@ -21,5 +21,10 @@ namespace Vanguard.App
() => _ = Flow.LoadLobbyAsync(), () => _ = Flow.LoadLobbyAsync(),
deck => _ = Flow.OpenLobbyWithQueueAsync(deck)); deck => _ = Flow.OpenLobbyWithQueueAsync(deck));
} }
private void OnDestroy()
{
_view?.Dispose();
}
} }
} }

View File

@ -111,5 +111,11 @@ namespace Vanguard.App
_queue.Hide(); _queue.Hide();
_store.Hide(); _store.Hide();
} }
private void OnDestroy()
{
_metaView?.Dispose();
_store?.Dispose();
}
} }
} }

View File

@ -12,11 +12,12 @@ using Vanguard.State;
namespace Vanguard.UI namespace Vanguard.UI
{ {
/// <summary>Коллекция игрока и разбор карт в пыль.</summary> /// <summary>Коллекция игрока и разбор карт в пыль.</summary>
public sealed class CollectionView public sealed class CollectionView : IDisposable
{ {
private readonly MetaService _meta; private readonly MetaService _meta;
private readonly PlayerState _player; private readonly PlayerState _player;
private readonly Action _onBack; private readonly Action _onBack;
private bool _disposed;
private readonly RectTransform _root; private readonly RectTransform _root;
private readonly Text _balances; private readonly Text _balances;
@ -106,6 +107,15 @@ namespace Vanguard.UI
_confirm.gameObject.SetActive(false); _confirm.gameObject.SetActive(false);
} }
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= OnPlayerChanged;
}
private RectTransform BuildConfirm(Transform parent) private RectTransform BuildConfirm(Transform parent)
{ {
var overlay = UiKit.CreatePanel(parent, "DismantleConfirm", new Color(0f, 0f, 0f, 0.72f)); var overlay = UiKit.CreatePanel(parent, "DismantleConfirm", new Color(0f, 0f, 0f, 0.72f));
@ -145,6 +155,9 @@ namespace Vanguard.UI
try try
{ {
var collection = await _meta.GetCollectionAsync(); var collection = await _meta.GetCollectionAsync();
if (_disposed)
return;
Rebuild(collection.Items ?? new List<CollectionItemDto>()); Rebuild(collection.Items ?? new List<CollectionItemDto>());
SetStatus( SetStatus(
collection.Items == null || collection.Items.Count == 0 collection.Items == null || collection.Items.Count == 0
@ -286,12 +299,18 @@ namespace Vanguard.UI
private void OnPlayerChanged() private void OnPlayerChanged()
{ {
if (_disposed || _balances == null)
return;
_balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust
+ " · Токены " + _player.Tokens; + " · Токены " + _player.Tokens;
} }
private void SetStatus(string text, Color color) private void SetStatus(string text, Color color)
{ {
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty; _status.text = text ?? string.Empty;
_status.color = color; _status.color = color;
} }

View File

@ -12,12 +12,13 @@ using Vanguard.State;
namespace Vanguard.UI namespace Vanguard.UI
{ {
/// <summary>Список колод и быстрый сборщик стартовой колоды из триал-деки.</summary> /// <summary>Список колод и быстрый сборщик стартовой колоды из триал-деки.</summary>
public sealed class DecksView public sealed class DecksView : IDisposable
{ {
private readonly MetaService _meta; private readonly MetaService _meta;
private readonly PlayerState _player; private readonly PlayerState _player;
private readonly Action _onBack; private readonly Action _onBack;
private readonly Action<DeckDto> _onPlay; private readonly Action<DeckDto> _onPlay;
private bool _disposed;
private readonly RectTransform _root; private readonly RectTransform _root;
private RectTransform _content; private RectTransform _content;
@ -122,6 +123,15 @@ namespace Vanguard.UI
_confirm.gameObject.SetActive(false); _confirm.gameObject.SetActive(false);
} }
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= OnPlayerChanged;
}
private RectTransform BuildConfirm(Transform parent) private RectTransform BuildConfirm(Transform parent)
{ {
var overlay = UiKit.CreatePanel(parent, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.72f)); var overlay = UiKit.CreatePanel(parent, "DeleteDeckConfirm", new Color(0f, 0f, 0f, 0.72f));
@ -161,6 +171,9 @@ namespace Vanguard.UI
try try
{ {
var decks = await _meta.GetDecksAsync(); var decks = await _meta.GetDecksAsync();
if (_disposed)
return;
Rebuild(decks); Rebuild(decks);
SetStatus( SetStatus(
decks.Count == 0 ? "Колод пока нет." : "Колод: " + decks.Count + ".", decks.Count == 0 ? "Колод пока нет." : "Колод: " + decks.Count + ".",
@ -324,12 +337,18 @@ namespace Vanguard.UI
private void OnPlayerChanged() private void OnPlayerChanged()
{ {
if (_disposed || _balances == null)
return;
_balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust
+ " · Токены " + _player.Tokens; + " · Токены " + _player.Tokens;
} }
private void SetStatus(string text, Color color) private void SetStatus(string text, Color color)
{ {
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty; _status.text = text ?? string.Empty;
_status.color = color; _status.color = color;
} }

View File

@ -229,6 +229,10 @@ namespace Vanguard.UI
private void SetBusy(bool busy, string status) private void SetBusy(bool busy, string status)
{ {
if (_loginButton == null || _registerButton == null || _serverUrl == null
|| _userName == null || _password == null)
return;
_busy = busy; _busy = busy;
_loginButton.interactable = !busy; _loginButton.interactable = !busy;
_registerButton.interactable = !busy; _registerButton.interactable = !busy;
@ -242,6 +246,9 @@ namespace Vanguard.UI
private void SetStatus(string text, Color color) private void SetStatus(string text, Color color)
{ {
if (_status == null)
return;
_status.text = text ?? string.Empty; _status.text = text ?? string.Empty;
_status.color = color; _status.color = color;
} }

View File

@ -9,7 +9,7 @@ using Vanguard.State;
namespace Vanguard.UI namespace Vanguard.UI
{ {
/// <summary>Главный экран после входа: профиль, балансы и заглушки разделов.</summary> /// <summary>Главный экран после входа: профиль, балансы и заглушки разделов.</summary>
public sealed class MetaView public sealed class MetaView : IDisposable
{ {
private readonly AuthService _auth; private readonly AuthService _auth;
private readonly PlayerState _player; private readonly PlayerState _player;
@ -19,6 +19,7 @@ namespace Vanguard.UI
private readonly Action _onStore; private readonly Action _onStore;
private readonly Action _onPlay; private readonly Action _onPlay;
private readonly Action _onOnboarding; private readonly Action _onOnboarding;
private bool _disposed;
private readonly RectTransform _root; private readonly RectTransform _root;
private readonly Text _playerName; private readonly Text _playerName;
@ -144,6 +145,15 @@ namespace Vanguard.UI
public void Hide() => UiKit.SetActive(_root, false); 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) private static Text AddStat(Transform parent, string caption)
{ {
var card = UiKit.CreatePanel(parent, "Stat_" + caption, new Color(0.09f, 0.11f, 0.15f, 1f)); var card = UiKit.CreatePanel(parent, "Stat_" + caption, new Color(0.09f, 0.11f, 0.15f, 1f));
@ -183,11 +193,15 @@ namespace Vanguard.UI
try try
{ {
await _auth.RefreshMeAsync(); await _auth.RefreshMeAsync();
if (_disposed)
return;
SetStatus("Профиль обновлён.", new Color(0.45f, 0.85f, 0.55f)); SetStatus("Профиль обновлён.", new Color(0.45f, 0.85f, 0.55f));
} }
catch (ApiException exception) when (exception.StatusCode == 401) catch (ApiException exception) when (exception.StatusCode == 401)
{ {
_onLoggedOut(); if (!_disposed)
_onLoggedOut();
} }
catch (Exception exception) catch (Exception exception)
{ {
@ -202,6 +216,8 @@ namespace Vanguard.UI
private void UpdateTexts() private void UpdateTexts()
{ {
if (_disposed)
return;
_playerName.text = string.IsNullOrEmpty(_player.UserName) _playerName.text = string.IsNullOrEmpty(_player.UserName)
? "…" ? "…"
: _player.UserName; : _player.UserName;
@ -224,6 +240,9 @@ namespace Vanguard.UI
private void SetStatus(string text, Color color) private void SetStatus(string text, Color color)
{ {
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty; _status.text = text ?? string.Empty;
_status.color = color; _status.color = color;
} }
@ -232,6 +251,9 @@ namespace Vanguard.UI
{ {
_logout.interactable = false; _logout.interactable = false;
await _auth.LogoutAsync(); await _auth.LogoutAsync();
if (_disposed)
return;
_logout.interactable = true; _logout.interactable = true;
_onLoggedOut(); _onLoggedOut();
} }

View File

@ -12,12 +12,13 @@ using Vanguard.State;
namespace Vanguard.UI namespace Vanguard.UI
{ {
/// <summary>Магазин в Lobby: бустеры и вторая триал-дека.</summary> /// <summary>Магазин в Lobby: бустеры и вторая триал-дека.</summary>
public sealed class StoreView public sealed class StoreView : IDisposable
{ {
private readonly MetaService _meta; private readonly MetaService _meta;
private readonly AuthService _auth; private readonly AuthService _auth;
private readonly PlayerState _player; private readonly PlayerState _player;
private readonly Action _onBack; private readonly Action _onBack;
private bool _disposed;
private readonly RectTransform _root; private readonly RectTransform _root;
private readonly Text _balances; private readonly Text _balances;
@ -147,6 +148,15 @@ namespace Vanguard.UI
UiKit.SetActive(_root, false); UiKit.SetActive(_root, false);
} }
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= OnPlayerChanged;
}
public void Tick() public void Tick()
{ {
} }
@ -164,6 +174,9 @@ namespace Vanguard.UI
{ {
await _auth.EnsureAccessAsync(); await _auth.EnsureAccessAsync();
_store = await _meta.GetStoreAsync(); _store = await _meta.GetStoreAsync();
if (_disposed)
return;
RebuildTrials(_store.TrialDecks); RebuildTrials(_store.TrialDecks);
_goldButton.GetComponentInChildren<Text>().text = _goldButton.GetComponentInChildren<Text>().text =
"Купить за " + _store.BoosterPriceGold + " Gold"; "Купить за " + _store.BoosterPriceGold + " Gold";
@ -319,6 +332,9 @@ namespace Vanguard.UI
private async Task BuyBoosterAsync(string payment) private async Task BuyBoosterAsync(string payment)
{ {
var result = await _meta.BuyBoosterAsync(payment); var result = await _meta.BuyBoosterAsync(payment);
if (_disposed)
return;
SetStatus("Бустер открыт!", new Color(0.45f, 0.85f, 0.55f)); SetStatus("Бустер открыт!", new Color(0.45f, 0.85f, 0.55f));
ShowResult(result); ShowResult(result);
} }
@ -326,6 +342,9 @@ namespace Vanguard.UI
private async Task BuyTrialAsync(StoreTrialDeckDto deck) private async Task BuyTrialAsync(StoreTrialDeckDto deck)
{ {
await _meta.BuyTrialDeckAsync(deck.Id); await _meta.BuyTrialDeckAsync(deck.Id);
if (_disposed)
return;
SetStatus("«" + deck.Name + "» добавлена в коллекцию.", new Color(0.45f, 0.85f, 0.55f)); SetStatus("«" + deck.Name + "» добавлена в коллекцию.", new Color(0.45f, 0.85f, 0.55f));
await LoadAsync(); await LoadAsync();
} }
@ -437,6 +456,9 @@ namespace Vanguard.UI
private void SetButtons(bool interactable) private void SetButtons(bool interactable)
{ {
if (_disposed)
return;
if (_goldButton != null) if (_goldButton != null)
_goldButton.interactable = interactable; _goldButton.interactable = interactable;
if (_tokenButton != null) if (_tokenButton != null)
@ -445,12 +467,18 @@ namespace Vanguard.UI
private void OnPlayerChanged() private void OnPlayerChanged()
{ {
if (_disposed || _balances == null)
return;
_balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust _balances.text = "Gold " + _player.Gold + " · Пыль " + _player.Dust
+ " · Токены " + _player.Tokens; + " · Токены " + _player.Tokens;
} }
private void SetStatus(string text, Color color) private void SetStatus(string text, Color color)
{ {
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty; _status.text = text ?? string.Empty;
_status.color = color; _status.color = color;
} }

View File

@ -3,6 +3,7 @@ using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using Vanguard.Api; using Vanguard.Api;
using Vanguard.Api.Contracts;
using Vanguard.App; using Vanguard.App;
using Vanguard.Auth; using Vanguard.Auth;
using Vanguard.Meta; using Vanguard.Meta;
@ -55,6 +56,19 @@ namespace Vanguard.Tests
Assert.IsTrue(Exists("QueueScreen")); Assert.IsTrue(Exists("QueueScreen"));
Assert.IsTrue(Exists("BattleScreen")); 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(); Cleanup();
} }