564 lines
18 KiB
C#
564 lines
18 KiB
C#
using PCL.Core.App;
|
|||
|
|
using PCL.Core.App.Localization;
|
||
|
|
using PCL.Core.Link.Natayark;
|
||
|
|
using PCL.Core.Link.Scaffolding;
|
||
|
|
using PCL.Core.Link.Scaffolding.Client.Models;
|
||
|
|
using PCL.Core.Link.Scaffolding.EasyTier;
|
||
|
|
using PCL.Core.Logging;
|
||
|
|
using PCL.Core.Utils;
|
||
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Collections.ObjectModel;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Net;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using System.Windows;
|
||
|
|
using PCL.Core.App.IoC;
|
||
|
|
using PCL.Core.UI;
|
||
|
|
using PCL.Core.Link.McPing;
|
||
|
|
|
||
|
|
namespace PCL.Core.Link.Lobby;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Lobby server. For auto-management
|
||
|
|
/// </summary>
|
||
|
|
[LifecycleService(LifecycleState.Loaded)]
|
||
|
|
public class LobbyService() : GeneralService("lobby", "LobbyService")
|
||
|
|
{
|
||
|
|
private static readonly LobbyController _LobbyController = new();
|
||
|
|
private static CancellationTokenSource _lobbyCts = new();
|
||
|
|
|
||
|
|
private static Task? _discoveringTask;
|
||
|
|
private static CancellationTokenSource _discoveringCts = new();
|
||
|
|
|
||
|
|
private static readonly Timer _ServerGameWatcher =
|
||
|
|
new(_CheckGameState, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
|
||
|
|
|
||
|
|
private static bool _isGameWatcherRunnable = false;
|
||
|
|
private static int _isLeaving;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Current lobby state.
|
||
|
|
/// </summary>
|
||
|
|
public static LobbyState CurrentState { get; private set; } = LobbyState.Idle;
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Founded local Minecraft worlds.
|
||
|
|
/// </summary>
|
||
|
|
public static ObservableCollection<FoundWorld> DiscoveredWorlds { get; } = [];
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Current players in current lobby.
|
||
|
|
/// </summary>
|
||
|
|
public static ObservableCollection<PlayerProfile> Players { get; private set; } = [];
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Demonstrate whether the current user is the host of the lobby.
|
||
|
|
/// </summary>
|
||
|
|
public static bool IsHost => _LobbyController.IsHost;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Current lobby full code.
|
||
|
|
/// </summary>
|
||
|
|
public static string? CurrentLobbyCode { get; private set; }
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Current lobby username.
|
||
|
|
/// </summary>
|
||
|
|
public static string? CurrentUserName { get; private set; }
|
||
|
|
|
||
|
|
#region UI Events
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Invoked when lobby state changed. (first arg is the old state; second arg is the new state.)
|
||
|
|
/// </summary>
|
||
|
|
public static event Action<LobbyState, LobbyState>? StateChanged;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Invoked when need to download EasyTier core files.
|
||
|
|
/// </summary>
|
||
|
|
public static event Action? OnNeedDownloadEasyTier;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Invoked when user stop the game in server mode.
|
||
|
|
/// </summary>
|
||
|
|
public static event Action? OnUserStopGame;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Invoked when client ping happened.
|
||
|
|
/// </summary>
|
||
|
|
public static event Action<long>? OnClientPing;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Invoked when server shut down.
|
||
|
|
/// </summary>
|
||
|
|
public static event Action? OnServerShutDown;
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Invoked when server started successfully.
|
||
|
|
/// </summary>
|
||
|
|
public static event Action? OnServerStarted;
|
||
|
|
|
||
|
|
public static event Action<Exception>? OnServerException;
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
/// <inheritdoc />
|
||
|
|
public override void Stop()
|
||
|
|
{
|
||
|
|
_lobbyCts.Cancel();
|
||
|
|
_LobbyController.CloseAsync().GetAwaiter().GetResult();
|
||
|
|
_ServerGameWatcher.Dispose();
|
||
|
|
_lobbyCts.Dispose();
|
||
|
|
|
||
|
|
_discoveringCts.Cancel();
|
||
|
|
if (_discoveringTask is not null)
|
||
|
|
{
|
||
|
|
Task.WhenAll(_discoveringTask);
|
||
|
|
_discoveringTask.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
_discoveringCts.Dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool _IsEasyTierCoreFileNotExist() =>
|
||
|
|
!File.Exists(Path.Combine(EasyTierMetadata.EasyTierFilePath, "easytier-core.exe")) &&
|
||
|
|
!File.Exists(Path.Combine(EasyTierMetadata.EasyTierFilePath, "Packet.dll")) &&
|
||
|
|
!File.Exists(Path.Combine(EasyTierMetadata.EasyTierFilePath, "easytier-cli.exe"));
|
||
|
|
|
||
|
|
|
||
|
|
public static async Task InitializeAsync()
|
||
|
|
{
|
||
|
|
if (CurrentState is not LobbyState.Idle && CurrentState is not LobbyState.Error)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_SetState(LobbyState.Initializing);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (_IsEasyTierCoreFileNotExist())
|
||
|
|
{
|
||
|
|
LogWrapper.Info("LobbyService", "EasyTier not found, starting download.");
|
||
|
|
OnNeedDownloadEasyTier?.Invoke();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
LogWrapper.Info("LobbyService", "EasyTier files check completed.");
|
||
|
|
}
|
||
|
|
|
||
|
|
// refresh naid token
|
||
|
|
var naidRefreshToken = States.Link.NaidRefreshToken;
|
||
|
|
if (!string.IsNullOrWhiteSpace(naidRefreshToken))
|
||
|
|
{
|
||
|
|
var expTime = States.Link.NaidRefreshExpireTime;
|
||
|
|
if (!string.IsNullOrWhiteSpace(expTime) &&
|
||
|
|
Convert.ToDateTime(expTime).CompareTo(DateTime.Now) < 0)
|
||
|
|
{
|
||
|
|
States.Link.NaidRefreshToken = string.Empty;
|
||
|
|
HintWrapper.Show(Lang.Text("Tools.GameLink.Natayark.TokenExpired"), HintTheme.Error);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
await NatayarkProfileManager.GetNaidDataAsync(naidRefreshToken, true).ConfigureAwait(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_SetState(LobbyState.Initialized);
|
||
|
|
LogWrapper.Info("LobbyService", "Lobby service initialized successfully.");
|
||
|
|
|
||
|
|
_ = DiscoverWorldAsync();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
LogWrapper.Error(ex, "LobbyService", "Lobby service initialization failed.");
|
||
|
|
HintWrapper.Show(Lang.Text("Link.Lobby.InitFailed"), HintTheme.Error);
|
||
|
|
_SetState(LobbyState.Error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Discover minecraft shared world.
|
||
|
|
/// </summary>
|
||
|
|
public static async Task DiscoverWorldAsync()
|
||
|
|
{
|
||
|
|
if (_discoveringCts.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (CurrentState is not LobbyState.Initialized && CurrentState is not LobbyState.Idle)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_SetState(LobbyState.Discovering);
|
||
|
|
await _RunInUiAsync(() => DiscoveredWorlds.Clear()).ConfigureAwait(false);
|
||
|
|
|
||
|
|
_discoveringTask = Task.Run(async () =>
|
||
|
|
{
|
||
|
|
var recordedPorts = new ConcurrentSet<int>();
|
||
|
|
using var listener = new BroadcastListener();
|
||
|
|
|
||
|
|
var handler = new Action<BroadcastRecord, IPEndPoint>((info, _) => Task.Run(async () =>
|
||
|
|
{
|
||
|
|
if (!recordedPorts.TryAdd(info.Address.Port)) return;
|
||
|
|
|
||
|
|
using var pinger = McPingServiceFactory.CreateService(new IPEndPoint(IPAddress.Loopback, info.Address.Port));
|
||
|
|
using var cts = new CancellationTokenSource(2000);
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var pingRes = await pinger.PingAsync(cts.Token).ConfigureAwait(false);
|
||
|
|
|
||
|
|
if (pingRes is null)
|
||
|
|
{
|
||
|
|
throw new ArgumentNullException(nameof(pingRes), "Failed to ping minecraft entity.");
|
||
|
|
}
|
||
|
|
|
||
|
|
var worldName = Lang.Text("Link.Lobby.WorldNameFormat",
|
||
|
|
pingRes.Description,
|
||
|
|
pingRes.Version.Name,
|
||
|
|
info.Address.Port);
|
||
|
|
await _RunInUiAsync(() => DiscoveredWorlds.Add(new FoundWorld(worldName, info.Address.Port)))
|
||
|
|
.ConfigureAwait(false);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
LogWrapper.Error(ex, "LobbyService", $"Pinging port {info.Address.Port} failed.");
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
|
||
|
|
listener.OnReceive += handler;
|
||
|
|
listener.Start();
|
||
|
|
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
|
||
|
|
listener.OnReceive -= handler;
|
||
|
|
}, _discoveringCts.Token);
|
||
|
|
|
||
|
|
_SetState(LobbyState.Initialized);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool _NotHaveNaid() =>
|
||
|
|
LobbyInfoProvider.RequiresLogin &&
|
||
|
|
string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.AccessToken);
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Create a new lobby.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="port">Minecraft share port.</param>
|
||
|
|
/// <param name="username">Player name.</param>
|
||
|
|
public static async Task<bool> CreateLobbyAsync(int port, string username)
|
||
|
|
{
|
||
|
|
if (_NotHaveNaid())
|
||
|
|
{
|
||
|
|
HintWrapper.Show(Lang.Text("Link.Lobby.LoginRequired"), HintTheme.Error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
await _discoveringCts.CancelAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
_SetState(LobbyState.Creating);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
CurrentUserName = username;
|
||
|
|
|
||
|
|
var serverEntity = await _LobbyController.LaunchServerAsync(username, port).ConfigureAwait(false);
|
||
|
|
if (serverEntity is null)
|
||
|
|
{
|
||
|
|
HintWrapper.Show(Lang.Text("Link.Lobby.CreateRoomFailed"), HintTheme.Error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
CurrentLobbyCode = serverEntity.EasyTier.Lobby.FullCode;
|
||
|
|
|
||
|
|
serverEntity.Server.ServerStopped += () => OnServerShutDown?.Invoke();
|
||
|
|
serverEntity.Server.PlayerProfilePing += _ServerOnPlayerPing;
|
||
|
|
serverEntity.Server.ServerStarted += _ServerOnServerStarted;
|
||
|
|
serverEntity.Server.ServerException += _ServerOnServerException;
|
||
|
|
//serverEntity.EasyTier.EasyTierProcessExisted += () =>
|
||
|
|
//{
|
||
|
|
// OnHint?.Invoke("EasyTierCore异常退出", CoreHintType.Critical);
|
||
|
|
// OnServerShutDown?.Invoke();
|
||
|
|
//}; this code will be invoked when EasyTier process successfully exited, not in failed state.
|
||
|
|
|
||
|
|
serverEntity.Server.Start();
|
||
|
|
|
||
|
|
_SetState(LobbyState.Connected);
|
||
|
|
_isGameWatcherRunnable = true;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
LogWrapper.Error(ex, "LobbyService", "Failed to create lobby.");
|
||
|
|
HintWrapper.Show(Lang.Text("Link.Lobby.CreateLobbyFailed"), HintTheme.Error);
|
||
|
|
await LeaveLobbyAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _ServerOnServerException(Exception? ex)
|
||
|
|
{
|
||
|
|
if (ex is null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
OnServerException?.Invoke(ex);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _ServerOnServerStarted(IReadOnlyList<PlayerProfile> profiles)
|
||
|
|
{
|
||
|
|
LogWrapper.Debug("LobbyService", "Send server started event.");
|
||
|
|
OnServerStarted?.Invoke();
|
||
|
|
_ServerOnPlayerPing(profiles);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _ServerOnPlayerPing(IReadOnlyList<PlayerProfile> players)
|
||
|
|
{
|
||
|
|
_ = _RunInUiAsync(() =>
|
||
|
|
{
|
||
|
|
var currentMachineIds = new HashSet<string>(Players.Select(p => p.MachineId));
|
||
|
|
var newMachineIds = new HashSet<string>(players.Select(p => p.MachineId));
|
||
|
|
|
||
|
|
if (currentMachineIds.SetEquals(newMachineIds))
|
||
|
|
{
|
||
|
|
LogWrapper.Debug("Player list has not changed");
|
||
|
|
return; // nothing was changed
|
||
|
|
}
|
||
|
|
|
||
|
|
LogWrapper.Debug("LobbyService", "Player list membership has changed, updating UI.");
|
||
|
|
|
||
|
|
var sortedNewPlayers = PlayerListHandler.Sort(players);
|
||
|
|
|
||
|
|
var idsToRemove = currentMachineIds.Except(newMachineIds).ToList();
|
||
|
|
if (idsToRemove.Any())
|
||
|
|
{
|
||
|
|
var playersToRemove = Players.Where(p => idsToRemove.Contains(p.MachineId)).ToList();
|
||
|
|
foreach (var player in playersToRemove)
|
||
|
|
{
|
||
|
|
Players.Remove(player);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var idsToAdd = newMachineIds.Except(currentMachineIds).ToList();
|
||
|
|
if (idsToAdd.Any())
|
||
|
|
{
|
||
|
|
var playersToAdd = sortedNewPlayers.Where(p => idsToAdd.Contains(p.MachineId)).ToList();
|
||
|
|
foreach (var player in playersToAdd)
|
||
|
|
{
|
||
|
|
Players.Add(player);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Join an exist lobby.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="lobbyCode">Lobby share code.</param>
|
||
|
|
/// <param name="username">Current use name.</param>
|
||
|
|
public static async Task<bool> JoinLobbyAsync(string lobbyCode, string username)
|
||
|
|
{
|
||
|
|
await _discoveringCts.CancelAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
_SetState(LobbyState.Joining);
|
||
|
|
|
||
|
|
LogWrapper.Info("LobbyService", $"Try to join lobby {lobbyCode}");
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
CurrentUserName = username;
|
||
|
|
CurrentLobbyCode = lobbyCode;
|
||
|
|
|
||
|
|
var clientEntity = await _LobbyController.LaunchClientAsync(username, lobbyCode, _lobbyCts.Token).ConfigureAwait(false);
|
||
|
|
|
||
|
|
if (clientEntity is null)
|
||
|
|
{
|
||
|
|
throw new InvalidOperationException(
|
||
|
|
Lang.Text("Link.Lobby.JoinFailed"));
|
||
|
|
}
|
||
|
|
|
||
|
|
clientEntity.Client.Heartbeat += _ClientOnHeartbeat;
|
||
|
|
clientEntity.Client.ServerShuttedDown += _ClientOnServerShutDown;
|
||
|
|
|
||
|
|
_SetState(LobbyState.Connected);
|
||
|
|
}
|
||
|
|
catch (ArgumentException codeEx)
|
||
|
|
{
|
||
|
|
LogWrapper.Error(codeEx, "LobbyService", $"Failed to join lobby {lobbyCode}.");
|
||
|
|
HintWrapper.Show(Lang.Text("Link.Lobby.InvalidCodeFormat"), HintTheme.Error);
|
||
|
|
await LeaveLobbyAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
catch (OperationCanceledException)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
LogWrapper.Error(ex, "LobbyService", $"Failed to join lobby {lobbyCode}.");
|
||
|
|
HintWrapper.Show(ex.Message, HintTheme.Error);
|
||
|
|
await LeaveLobbyAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _ClientOnServerShutDown()
|
||
|
|
{
|
||
|
|
OnServerShutDown?.Invoke();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _ClientOnHeartbeat(IReadOnlyList<PlayerProfile> players, long latency)
|
||
|
|
{
|
||
|
|
_ = _RunInUiAsync(() =>
|
||
|
|
{
|
||
|
|
var currentMachineIds = new HashSet<string>(Players.Select(p => p.MachineId));
|
||
|
|
var newMachineIds = new HashSet<string>(players.Select(p => p.MachineId));
|
||
|
|
|
||
|
|
if (currentMachineIds.SetEquals(newMachineIds))
|
||
|
|
{
|
||
|
|
return; // nothing was changed
|
||
|
|
}
|
||
|
|
|
||
|
|
LogWrapper.Debug("LobbyService", "Player list membership has changed, updating UI.");
|
||
|
|
|
||
|
|
var sortedNewPlayers = PlayerListHandler.Sort(players);
|
||
|
|
|
||
|
|
var idsToRemove = currentMachineIds.Except(newMachineIds).ToList();
|
||
|
|
if (idsToRemove.Any())
|
||
|
|
{
|
||
|
|
var playersToRemove = Players.Where(p => idsToRemove.Contains(p.MachineId)).ToList();
|
||
|
|
foreach (var player in playersToRemove)
|
||
|
|
{
|
||
|
|
Players.Remove(player);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var idsToAdd = newMachineIds.Except(currentMachineIds).ToList();
|
||
|
|
if (idsToAdd.Any())
|
||
|
|
{
|
||
|
|
var playersToAdd = sortedNewPlayers.Where(p => idsToAdd.Contains(p.MachineId)).ToList();
|
||
|
|
foreach (var player in playersToAdd)
|
||
|
|
{
|
||
|
|
Players.Add(player);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
OnClientPing?.Invoke(latency);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Leave from lobby.
|
||
|
|
/// </summary>
|
||
|
|
public static async Task LeaveLobbyAsync()
|
||
|
|
{
|
||
|
|
if (Interlocked.Exchange(ref _isLeaving, 1) == 1)
|
||
|
|
return;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_SetState(LobbyState.Leaving);
|
||
|
|
|
||
|
|
await _lobbyCts.CancelAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
Players.Clear();
|
||
|
|
CurrentLobbyCode = null;
|
||
|
|
CurrentUserName = null;
|
||
|
|
|
||
|
|
if (_LobbyController.ScfClientEntity?.Client is not null)
|
||
|
|
{
|
||
|
|
_LobbyController.ScfClientEntity.Client.Heartbeat -= _ClientOnHeartbeat;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (_LobbyController.ScfServerEntity?.Server is not null)
|
||
|
|
{
|
||
|
|
_LobbyController.ScfServerEntity.Server.PlayerProfilePing -= _ServerOnPlayerPing;
|
||
|
|
_LobbyController.ScfServerEntity.Server.ServerStarted -= _ServerOnServerStarted;
|
||
|
|
}
|
||
|
|
|
||
|
|
await _LobbyController.CloseAsync().ConfigureAwait(false);
|
||
|
|
|
||
|
|
|
||
|
|
_lobbyCts = new CancellationTokenSource();
|
||
|
|
_SetState(LobbyState.Initialized);
|
||
|
|
|
||
|
|
LogWrapper.Info("LobbyService", "Left lobby and cleaned up resources.");
|
||
|
|
|
||
|
|
_isGameWatcherRunnable = false;
|
||
|
|
|
||
|
|
_discoveringCts = new CancellationTokenSource();
|
||
|
|
_ = DiscoverWorldAsync();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
LogWrapper.Error(ex, "LobbyService", "Failed when leave lobby.");
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
_isLeaving = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _SetState(LobbyState newState)
|
||
|
|
{
|
||
|
|
var oldState = CurrentState;
|
||
|
|
if (oldState == newState)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
CurrentState = newState;
|
||
|
|
|
||
|
|
LogWrapper.Info("LobbyService", $"Lobby state changed from {oldState} to {newState}");
|
||
|
|
|
||
|
|
StateChanged?.Invoke(oldState, newState);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void _CheckGameState(object? state)
|
||
|
|
{
|
||
|
|
if (!_isGameWatcherRunnable)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (_LobbyController.ScfServerEntity is null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
LobbyController.IsHostInstanceAvailableAsync(_LobbyController.ScfServerEntity.EasyTier.MinecraftPort)
|
||
|
|
.ContinueWith(async (task) =>
|
||
|
|
{
|
||
|
|
var isExist = await task.ConfigureAwait(false);
|
||
|
|
if (!isExist)
|
||
|
|
{
|
||
|
|
_isGameWatcherRunnable = false;
|
||
|
|
OnUserStopGame?.Invoke();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task _RunInUiAsync(Action action)
|
||
|
|
{
|
||
|
|
await Application.Current.Dispatcher.InvokeAsync(action);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Founded minecraft world information.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="Name">World name.</param>
|
||
|
|
/// <param name="Port">World share port.</param>
|
||
|
|
public record FoundWorld(string Name, int Port);
|