Add blocking reconnect overlay with automatic retries
CI / test (push) Successful in 2m13s
Details
CI / test (push) Successful in 2m13s
Details
This commit is contained in:
parent
1c0cb4ecb0
commit
e81b966867
|
|
@ -60,6 +60,12 @@ namespace Vanguard.UI
|
|||
|
||||
private GameConnection _connection;
|
||||
private CancellationTokenSource _cts;
|
||||
private CancellationTokenSource _reconnectCts;
|
||||
private bool _reconnecting;
|
||||
private int _reconnectAttempt;
|
||||
private RectTransform _reconnectOverlay;
|
||||
private Text _reconnectOverlayText;
|
||||
private Text _reconnectOverlayHint;
|
||||
private MatchConnectionInfo _info;
|
||||
private MatchView _view;
|
||||
private bool _connecting;
|
||||
|
|
@ -213,6 +219,7 @@ namespace Vanguard.UI
|
|||
BuildOverlaysAndZones();
|
||||
BuildPhasePanel();
|
||||
BuildCombatStats();
|
||||
BuildReconnectOverlay();
|
||||
_cardMenu = new CardActionMenuView();
|
||||
_cardMenu.Build(_root);
|
||||
|
||||
|
|
@ -233,6 +240,7 @@ namespace Vanguard.UI
|
|||
public void Show(MatchConnectionInfo info)
|
||||
{
|
||||
_info = info;
|
||||
CancelReconnect();
|
||||
_view = null;
|
||||
_finished = false;
|
||||
_confirmConcede = false;
|
||||
|
|
@ -286,6 +294,7 @@ namespace Vanguard.UI
|
|||
|
||||
public void Hide()
|
||||
{
|
||||
CancelReconnect();
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
|
|
@ -294,6 +303,7 @@ namespace Vanguard.UI
|
|||
{
|
||||
var connection = _connection;
|
||||
_connection = null;
|
||||
DetachConnection(connection);
|
||||
_ = connection.DisposeAsync().AsTask();
|
||||
}
|
||||
|
||||
|
|
@ -550,23 +560,25 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
private async Task ConnectAsync()
|
||||
private async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connecting || _info == null)
|
||||
return;
|
||||
return false;
|
||||
|
||||
_connecting = true;
|
||||
_reconnect.gameObject.SetActive(false);
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = new CancellationTokenSource();
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (_connection != null)
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
var previous = _connection;
|
||||
_connection = null;
|
||||
DetachConnection(previous);
|
||||
await previous.DisposeAsync();
|
||||
}
|
||||
|
||||
var connection = new GameConnection();
|
||||
|
|
@ -586,16 +598,20 @@ namespace Vanguard.UI
|
|||
_cts.Token);
|
||||
|
||||
SetStatus("Соединение открыто. Ожидаем состояние…", Muted);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// экран закрыли
|
||||
return false;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
SetStatus("Не удалось подключиться: " + exception.Message, new Color(1f, 0.55f, 0.45f));
|
||||
_reconnect.gameObject.SetActive(true);
|
||||
_reconnect.gameObject.SetActive(false);
|
||||
if (!_reconnecting)
|
||||
BeginReconnect("Не удалось подключиться: " + exception.Message);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -603,10 +619,25 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
private void DetachConnection(GameConnection connection)
|
||||
{
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
connection.HelloReceived -= OnHello;
|
||||
connection.SnapshotReceived -= OnSnapshot;
|
||||
connection.EventsReceived -= OnEvents;
|
||||
connection.ClockReceived -= OnClock;
|
||||
connection.ErrorReceived -= OnError;
|
||||
connection.Closed -= OnClosed;
|
||||
}
|
||||
|
||||
private void OnHello(ServerHello hello)
|
||||
{
|
||||
if (_info != null)
|
||||
_info.Seat = hello.PlayerIndex;
|
||||
if (_reconnecting)
|
||||
FinishReconnect();
|
||||
}
|
||||
|
||||
private void OnSnapshot(ServerSnapshot snapshot)
|
||||
|
|
@ -614,6 +645,9 @@ namespace Vanguard.UI
|
|||
if (!_root.gameObject.activeSelf || snapshot?.View == null)
|
||||
return;
|
||||
|
||||
if (_reconnecting)
|
||||
FinishReconnect();
|
||||
|
||||
_view = snapshot.View;
|
||||
_finished = _view.IsFinished;
|
||||
_pendingIntents.Clear();
|
||||
|
|
@ -634,6 +668,9 @@ namespace Vanguard.UI
|
|||
{
|
||||
foreach (var envelope in events.Events)
|
||||
{
|
||||
if (envelope.Event is GameFinishedView)
|
||||
_finished = true;
|
||||
|
||||
var line = Describe(envelope.Event);
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
_logLines.Add(line);
|
||||
|
|
@ -776,11 +813,142 @@ namespace Vanguard.UI
|
|||
|
||||
private void OnClosed(string reason)
|
||||
{
|
||||
if (!_root.gameObject.activeSelf || _finished)
|
||||
if (!_root.gameObject.activeSelf || _finished || _view?.IsFinished == true)
|
||||
return;
|
||||
|
||||
SetStatus("Соединение потеряно: " + reason, new Color(1f, 0.60f, 0.45f));
|
||||
_reconnect.gameObject.SetActive(true);
|
||||
BeginReconnect(reason);
|
||||
}
|
||||
|
||||
private void BeginReconnect(string reason)
|
||||
{
|
||||
if (_info == null
|
||||
|| !_root.gameObject.activeSelf
|
||||
|| _finished
|
||||
|| _view?.IsFinished == true)
|
||||
return;
|
||||
|
||||
if (_reconnecting)
|
||||
{
|
||||
SetReconnectHint("Соединение потеряно. Переподключаемся…");
|
||||
SetReconnectReason(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
_reconnecting = true;
|
||||
_reconnectAttempt = 0;
|
||||
_reconnectOverlay?.SetAsLastSibling();
|
||||
_reconnectOverlay?.gameObject.SetActive(true);
|
||||
SetReconnectHint("Переподключаемся…");
|
||||
SetReconnectReason(reason);
|
||||
_reconnect.gameObject.SetActive(false);
|
||||
_inputLock.Acquire("reconnect");
|
||||
|
||||
_reconnectCts?.Cancel();
|
||||
_reconnectCts?.Dispose();
|
||||
_reconnectCts = new CancellationTokenSource();
|
||||
_ = ReconnectLoopAsync(_reconnectCts.Token);
|
||||
}
|
||||
|
||||
private async Task ReconnectLoopAsync(CancellationToken token)
|
||||
{
|
||||
var delays = new[] { 0f, 2f, 10f, 30f };
|
||||
var attempt = 0;
|
||||
|
||||
while (!token.IsCancellationRequested
|
||||
&& _root.gameObject.activeSelf
|
||||
&& !_finished)
|
||||
{
|
||||
var delay = attempt < delays.Length ? delays[attempt] : 30f;
|
||||
if (delay > 0f)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
attempt++;
|
||||
_reconnectAttempt = attempt;
|
||||
SetReconnectHint("Переподключение, попытка " + attempt + "…");
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _meta.CreateTicketAsync(_info.MatchId);
|
||||
_info.Ticket = ticket.Ticket;
|
||||
_info.WsUrl = ticket.WsUrl;
|
||||
|
||||
if (await ConnectAsync(token))
|
||||
{
|
||||
SetReconnectHint("Соединение восстановлено. Загружаем поле…");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
SetReconnectHint("Попытка " + attempt + " не удалась. Повторяем…");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FinishReconnect()
|
||||
{
|
||||
if (!_reconnecting)
|
||||
return;
|
||||
|
||||
_reconnecting = false;
|
||||
_reconnectAttempt = 0;
|
||||
if (_reconnectOverlay != null)
|
||||
_reconnectOverlay.gameObject.SetActive(false);
|
||||
_inputLock.Release("reconnect");
|
||||
SetStatus("Соединение восстановлено.", Muted);
|
||||
}
|
||||
|
||||
private void SetReconnectHint(string text)
|
||||
{
|
||||
if (_reconnectOverlayHint != null)
|
||||
_reconnectOverlayHint.text = text ?? string.Empty;
|
||||
}
|
||||
|
||||
private void SetReconnectReason(string reason)
|
||||
{
|
||||
if (_reconnectOverlayText == null)
|
||||
return;
|
||||
|
||||
var value = reason;
|
||||
const string prefix = "Соединение потеряно: ";
|
||||
while (!string.IsNullOrEmpty(value)
|
||||
&& value.StartsWith(prefix, StringComparison.Ordinal))
|
||||
{
|
||||
value = value.Substring(prefix.Length);
|
||||
}
|
||||
|
||||
_reconnectOverlayText.text = string.IsNullOrWhiteSpace(value)
|
||||
? "Соединение с сервером прервано. Переподключаемся автоматически."
|
||||
: value;
|
||||
}
|
||||
|
||||
private void CancelReconnect()
|
||||
{
|
||||
_reconnecting = false;
|
||||
_reconnectAttempt = 0;
|
||||
_reconnectCts?.Cancel();
|
||||
_reconnectCts?.Dispose();
|
||||
_reconnectCts = null;
|
||||
if (_reconnectOverlay != null)
|
||||
_reconnectOverlay.gameObject.SetActive(false);
|
||||
_inputLock.Release("reconnect");
|
||||
}
|
||||
|
||||
private async Task RequestSnapshotAsync()
|
||||
|
|
@ -888,6 +1056,59 @@ namespace Vanguard.UI
|
|||
ApplyIntentPanelLayout();
|
||||
}
|
||||
|
||||
private void BuildReconnectOverlay()
|
||||
{
|
||||
_reconnectOverlay = UiKit.CreatePanel(
|
||||
_root,
|
||||
"ReconnectOverlay",
|
||||
new Color(0f, 0f, 0f, 0.86f));
|
||||
var card = UiKit.CreateCenteredCard(
|
||||
_reconnectOverlay,
|
||||
"ReconnectCard",
|
||||
new Vector2(680f, 280f),
|
||||
Panel);
|
||||
UiKit.AddVerticalLayout(card, 22, 10);
|
||||
|
||||
var title = UiKit.CreateText(
|
||||
card,
|
||||
"ReconnectTitle",
|
||||
"Соединение потеряно",
|
||||
26,
|
||||
new Color(1f, 0.78f, 0.42f),
|
||||
TextAnchor.MiddleCenter);
|
||||
title.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(title, 42f);
|
||||
|
||||
_reconnectOverlayHint = UiKit.CreateText(
|
||||
card,
|
||||
"ReconnectHint",
|
||||
"Переподключаемся…",
|
||||
18,
|
||||
Color.white,
|
||||
TextAnchor.MiddleCenter);
|
||||
UiKit.SetPreferredHeight(_reconnectOverlayHint, 32f);
|
||||
|
||||
_reconnectOverlayText = UiKit.CreateText(
|
||||
card,
|
||||
"ReconnectReason",
|
||||
string.Empty,
|
||||
14,
|
||||
new Color(0.72f, 0.80f, 0.94f),
|
||||
TextAnchor.MiddleCenter);
|
||||
_reconnectOverlayText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UiKit.SetPreferredHeight(_reconnectOverlayText, 64f);
|
||||
|
||||
var exit = UiKit.CreateButton(
|
||||
card,
|
||||
"ReconnectExit",
|
||||
"Выйти в меню",
|
||||
OnExitClicked,
|
||||
17);
|
||||
UiKit.SetPreferredHeight(exit, 44f);
|
||||
|
||||
_reconnectOverlay.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ApplyIntentPanelLayout()
|
||||
{
|
||||
var shift = _intentPanelVisible ? 0f : IntentPanelShift;
|
||||
|
|
@ -916,34 +1137,12 @@ namespace Vanguard.UI
|
|||
OnIntentClicked(new ConcedeIntent());
|
||||
}
|
||||
|
||||
private async void OnReconnectClicked()
|
||||
private void OnReconnectClicked()
|
||||
{
|
||||
if (_info == null)
|
||||
return;
|
||||
|
||||
_reconnect.interactable = false;
|
||||
SetStatus("Получаем новый тикет…", Muted);
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _meta.CreateTicketAsync(_info.MatchId);
|
||||
_info.Ticket = ticket.Ticket;
|
||||
_info.WsUrl = ticket.WsUrl;
|
||||
await ConnectAsync();
|
||||
}
|
||||
catch (ApiException exception)
|
||||
{
|
||||
SetStatus(exception.Message, new Color(1f, 0.55f, 0.45f));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
SetStatus("Переподключение не удалось.", new Color(1f, 0.55f, 0.45f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_reconnect.interactable = true;
|
||||
}
|
||||
BeginReconnect("Переподключение…");
|
||||
}
|
||||
|
||||
private void OnExitClicked()
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ PlayerSettings:
|
|||
androidApplicationEntry: 1
|
||||
defaultIsNativeResolution: 0
|
||||
macRetinaSupport: 1
|
||||
runInBackground: 0
|
||||
runInBackground: 1
|
||||
callOnDisableOnAssetBundleUnload: 1
|
||||
muteOtherAudioSources: 0
|
||||
Prepare IOS For Recording: 0
|
||||
|
|
|
|||
|
|
@ -1009,5 +1009,10 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- UI: reference-разрешение **1920×1080**, CanvasScaler `matchWidthOrHeight
|
||||
= 0.5`; standalone принудительно `1920×1080 FullScreenWindow`,
|
||||
`defaultIsNativeResolution = 0`.
|
||||
- Потеря WebSocket: полноэкранный `ReconnectOverlay` блокирует бой,
|
||||
автоматические попытки 0/2/10/30 с, после `ServerHello` overlay
|
||||
скрывается; в overlay есть выход в меню. Игра работает в фоне
|
||||
(`runInBackground = 1`), чтобы heartbeat не останавливался при потере
|
||||
фокуса.
|
||||
- Проверки: сервер **142** правил + **45** серверных, `validate` и
|
||||
`translations check` OK; клиент EditMode **30/30**.
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@
|
|||
overlay, а не выбирает случайную.
|
||||
- В фазе защиты доступны «Защититься» (если есть допустимые карты) и
|
||||
«Не защищаться»; выбор карты защиты и сброса идёт через overlay.
|
||||
- Локальный standalone работает в фоне (`runInBackground`), чтобы heartbeat
|
||||
WebSocket не останавливался при потере фокуса.
|
||||
- При потере соединения весь бой блокируется полноэкранным overlay
|
||||
«Соединение потеряно»; автоматический reconnect идёт с задержками
|
||||
0/2/10/30 с, после успеха overlay скрывается и поле показывается снова.
|
||||
Кнопка внутри overlay позволяет выйти в меню.
|
||||
- Правую панель действий можно временно скрыть кнопкой «Скрыть панель»
|
||||
в верхней строке; поле остаётся видимым, вернуть — «Показать панель».
|
||||
При скрытии панель фаз и блоки противника сдвигаются вправо.
|
||||
|
|
|
|||
Loading…
Reference in New Issue