Merge branch 'develop' into feature/clear-embedded-chip

# Conflicts:
#	Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs
#	Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs
This commit is contained in:
NguyenVanDat
2026-02-03 10:55:33 +07:00
57 changed files with 3779 additions and 94 deletions
@@ -14,6 +14,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BrewMonster.Scripts.Task;
using BrewMonster.UI;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -24,6 +25,8 @@ namespace BrewMonster.Network
// 2. Login
public class UnityGameSession : MonoSingleton<UnityGameSession>
{
private const string WorldSceneName = "a61";
private const string LoginSceneName = "LoginScene";
private GameSession _gameSession;
private bool _isInitialized = false;
@@ -82,6 +85,28 @@ namespace BrewMonster.Network
Instance._ip = ip;
Instance._port = port;
}
/// <summary>
/// Origin-like: In-game can only return to Select Role (LOGOUT(1)).
/// This will load LoginScene and auto-login (using saved creds) to show the Select Role UI.
/// </summary>
public static void ReturnToSelectRole()
{
if (Instance == null) return;
// Origin-like: in-game only returns to Select Role (auto-login using saved creds).
// Keep world scene loaded (a61) but cleaned.
_ = Instance.LogoutAndReturnAsync(outType: 1, entryTarget: LogoutFlowState.LoginEntryTarget.SelectRole, clearSavedCreds: false);
}
/// <summary>
/// Origin-like: Account logout from Select Role (LOGOUT(0)).
/// This will clear saved creds and load LoginScene showing username/password UI.
/// </summary>
public static void LogoutAccount()
{
if (Instance == null) return;
_ = Instance.LogoutAndReturnAsync(outType: 0, entryTarget: LogoutFlowState.LoginEntryTarget.LoginUI, clearSavedCreds: true);
}
public static void c2s_CmdCastSkill(int idSkill, byte byPVPMask, int iNumTarget, int[] aTargets)
{
Instance._gameSession.CmdCache.SendCmdCastSkill(idSkill, byPVPMask, iNumTarget, aTargets);
@@ -146,6 +171,166 @@ namespace BrewMonster.Network
DontDestroyOnLoad(gameObject);
}
private async Task LogoutAndReturnAsync(int outType, LogoutFlowState.LoginEntryTarget entryTarget, bool clearSavedCreds)
{
// Tell LoginScene what to show next.
LogoutFlowState.NextLoginEntry = entryTarget;
if (clearSavedCreds)
{
PlayerPrefs.DeleteKey("username");
PlayerPrefs.DeleteKey("password");
PlayerPrefs.Save();
}
try
{
if (_gameSession != null && _gameSession.IsConnected)
{
// Send LOGOUT(outType) like the original client.
_gameSession.SendPlayerLogout(outType);
// Wait briefly for server-driven disconnect.
await WaitForDisconnectAsync(timeoutMs: 4000);
}
}
catch (Exception ex)
{
BMLogger.LogError($"LogoutAndReturnAsync exception: {ex.Message}");
}
finally
{
// Fallback: if server didn't close, close locally.
if (_gameSession != null && _gameSession.IsConnected)
{
_gameSession.Disconnect();
}
}
// Return to LoginScene.
// IMPORTANT: for outType=1 we must keep the world scene (a61) loaded; only "clean" runtime objects.
await Task.Yield();
// Requirement: even on account logout, keep a61 loaded (do not delete the world scene),
// just clean runtime objects and show LoginScene UI.
CleanRuntimeObjectsKeepWorld();
await EnsureSceneLoadedAdditiveAsync(WorldSceneName);
await EnsureLoginSceneAdditiveAndActivateAsync(keepSceneName: WorldSceneName);
// When LoginScene is already loaded additively, Start() won't re-run.
// Force the login UI to refresh to the correct entry state immediately.
ApplyLoginEntryToUI(entryTarget);
}
private static void ApplyLoginEntryToUI(LogoutFlowState.LoginEntryTarget entryTarget)
{
try
{
// Find even if inactive (Resources.FindObjectsOfTypeAll returns inactive objects too).
var all = Resources.FindObjectsOfTypeAll<LoginScreenUI>();
for (int i = 0; i < all.Length; i++)
{
var ui = all[i];
if (ui == null) continue;
if (!ui.gameObject.scene.IsValid() || ui.gameObject.scene.name != LoginSceneName) continue;
// Avoid hard dependency on method existence (merges may edit LoginScreenUI).
ui.SendMessage("ApplyLoginEntry", entryTarget, SendMessageOptions.DontRequireReceiver);
return;
}
}
catch (Exception ex)
{
BMLogger.LogError($"ApplyLoginEntryToUI error: {ex.Message}");
}
}
private static void CleanRuntimeObjectsKeepWorld()
{
// Spawned runtime objects (player/monsters/vfx) are parented under ObjectSpawner.
// Destroying them "cleans" the world without unloading the scene (a61 remains loaded).
try
{
var spawner = BrewMonster.ObjectSpawner.Instance;
if (spawner == null) return;
var root = spawner.transform;
for (int i = root.childCount - 1; i >= 0; i--)
{
var child = root.GetChild(i);
if (child != null)
UnityEngine.Object.Destroy(child.gameObject);
}
}
catch (Exception ex)
{
BMLogger.LogError($"CleanRuntimeObjectsKeepWorld error: {ex.Message}");
}
}
private static async Task EnsureSceneLoadedAdditiveAsync(string sceneName)
{
if (string.IsNullOrEmpty(sceneName)) return;
var s = SceneManager.GetSceneByName(sceneName);
if (s.IsValid() && s.isLoaded) return;
var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (op != null && !op.isDone)
await Task.Yield();
}
private static async Task EnsureLoginSceneAdditiveAndActivateAsync(string keepSceneName)
{
// Load LoginScene additively if needed (do not unload keepSceneName, e.g., a61).
var loginScene = SceneManager.GetSceneByName(LoginSceneName);
if (!loginScene.IsValid() || !loginScene.isLoaded)
{
var op = SceneManager.LoadSceneAsync(LoginSceneName, LoadSceneMode.Additive);
while (op != null && !op.isDone)
await Task.Yield();
loginScene = SceneManager.GetSceneByName(LoginSceneName);
}
if (loginScene.IsValid() && loginScene.isLoaded)
{
SceneManager.SetActiveScene(loginScene);
}
// Optionally unload other non-world scenes (keep a61 + LoginScene).
// This prevents extra scenes like NPCRender staying around.
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var s = SceneManager.GetSceneAt(i);
if (!s.IsValid() || !s.isLoaded) continue;
if (s.name == LoginSceneName) continue;
if (!string.IsNullOrEmpty(keepSceneName) && s.name == keepSceneName) continue;
// Only unload if it's not the active scene (we already switched active to LoginScene above).
if (SceneManager.GetActiveScene() == s) continue;
_ = SceneManager.UnloadSceneAsync(s);
}
}
private Task WaitForDisconnectAsync(int timeoutMs)
{
if (_gameSession == null || !_gameSession.IsConnected)
return Task.CompletedTask;
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
void OnDisc() => tcs.TrySetResult(true);
_gameSession.Disconnected += OnDisc;
return Task.Run(async () =>
{
try
{
await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs));
}
finally
{
_gameSession.Disconnected -= OnDisc;
}
});
}
public RoleInfo GetRoleInfo()
{
return _gameSession.GetRoleInfo();
@@ -486,10 +671,6 @@ namespace BrewMonster.Network
{
Instance._gameSession.c2s_SendCmdGivePresent(roleid, mail_id, goods_id, goods_index, goods_slot);
}
public static void c2s_CmdNPCSevClearEmbeddedChip(int iEquipIdx, int tidEquip)
{
Instance._gameSession.c2s_CmdNPCSevClearEmbeddedChip(iEquipIdx, tidEquip);
}
public void Update()
{
@@ -504,4 +685,33 @@ namespace BrewMonster.Network
Instance._gameSession.c2s_SendCmdGetItemInfo(byPackage, bySlot);
}
}
/// <summary>
/// Small cross-scene state to tell LoginScene what to show after a logout flow.
/// This mirrors the original client: in-game can only return to Select Role; Account Logout happens from Select Role.
/// </summary>
public static class LogoutFlowState
{
public enum LoginEntryTarget
{
LoginUI = 0,
SelectRole = 1,
}
private static LoginEntryTarget _nextEntry = LoginEntryTarget.LoginUI;
public static LoginEntryTarget NextLoginEntry
{
get => _nextEntry;
set => _nextEntry = value;
}
/// <summary>Consume and reset to default (LoginUI).</summary>
public static LoginEntryTarget ConsumeNextLoginEntry()
{
var v = _nextEntry;
_nextEntry = LoginEntryTarget.LoginUI;
return v;
}
}
}