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>(),
() => _ = Flow.LoadLobbyAsync());
}
private void OnDestroy()
{
_view?.Dispose();
}
}
}

View File

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

View File

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

View File

@ -12,11 +12,12 @@ using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Коллекция игрока и разбор карт в пыль.</summary>
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<CollectionItemDto>());
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;
}

View File

@ -12,12 +12,13 @@ using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Список колод и быстрый сборщик стартовой колоды из триал-деки.</summary>
public sealed class DecksView
public sealed class DecksView : IDisposable
{
private readonly MetaService _meta;
private readonly PlayerState _player;
private readonly Action _onBack;
private readonly Action<DeckDto> _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;
}

View File

@ -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;
}

View File

@ -9,7 +9,7 @@ using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Главный экран после входа: профиль, балансы и заглушки разделов.</summary>
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();
}

View File

@ -12,12 +12,13 @@ using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Магазин в Lobby: бустеры и вторая триал-дека.</summary>
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>().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;
}

View File

@ -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();
}