962 lines
36 KiB
C#
962 lines
36 KiB
C#
using BrewMonster;
|
|
using BrewMonster.Common;
|
|
using CSNetwork;
|
|
using CSNetwork.C2SCommand;
|
|
using CSNetwork.Protocols;
|
|
using CSNetwork.Protocols.RPCData;
|
|
using CSNetwork.Security;
|
|
using ModelRenderer.Scripts.Common;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using BrewMonster.Managers;
|
|
using BrewMonster.Scripts.Task;
|
|
using BrewMonster.UI;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using BrewMonster.Scripts;
|
|
using BrewMonster.Scripts.ChatUI;
|
|
|
|
namespace BrewMonster.Network
|
|
{
|
|
// How to connect to the server:
|
|
// 1. Set the connection info
|
|
// 2. Login
|
|
public class UnityGameSession : MonoSingleton<UnityGameSession>
|
|
{
|
|
private const string WorldSceneName = "a61";
|
|
private const string LoginSceneName = "LoginScene";
|
|
private GameSession _gameSession;
|
|
|
|
private bool _isInitialized = false;
|
|
private string _ip = "";
|
|
private int _port = 0;
|
|
private string _username = "";
|
|
private string _password = "";
|
|
private bool _isIntentionalDisconnect = false;
|
|
|
|
private bool _wasUnexpectedlyDisconnected = false;
|
|
// When true, prevent all outgoing protocols (background ticks, tasks, etc.) from sending.
|
|
private bool _suppressOutgoing = false;
|
|
|
|
CECStubbornFactionInfoSender m_stubbornFactionInfoSender;
|
|
public GameSession GameSession { get => _gameSession; }
|
|
public CECC2SCmdCache GetC2SCmdCache() { return _gameSession.CmdCache; }
|
|
#if UNITY_EDITOR
|
|
public bool isDebg;
|
|
private bool lastDebug;
|
|
|
|
|
|
public void OnValidate()
|
|
{
|
|
if (_gameSession != null && isDebg != lastDebug)
|
|
{
|
|
_gameSession.IsDebug = isDebg;
|
|
lastDebug = isDebg;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
protected override void Awake()
|
|
{
|
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
|
GameSession.Context = SynchronizationContext.Current;
|
|
// Khởi tạo ChatThreadDispatcher trên main thread trước khi luồng mạng gọi Instance.Post — English: warm singleton on main thread before network posts.
|
|
_ = ChatThreadDispatcher.Instance;
|
|
|
|
base.Awake();
|
|
}
|
|
private void Start()
|
|
{
|
|
CECNPC.InitStaticRes();
|
|
}
|
|
/// <summary>
|
|
/// Send a
|
|
/// </summary>
|
|
/// <param name="protocol"></param>
|
|
/// <param name="complete"></param>
|
|
public static void SendProtocol(Protocol protocol, Action complete = null)
|
|
{
|
|
if (!Instance._isInitialized)
|
|
{
|
|
return;
|
|
}
|
|
Instance._gameSession.SendProtocol(protocol, complete);
|
|
}
|
|
|
|
/// <summary>Set the connection info. This MUST be called call before login</summary>
|
|
public static void SetConnectionInfo(string ip, int port)
|
|
{
|
|
BMLogger.Log($"Set connection info {ip} {port}");
|
|
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: false);
|
|
}
|
|
public static void c2s_CmdCastSkill(int idSkill, byte byPVPMask, int iNumTarget, int[] aTargets)
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdCastSkill(idSkill, byPVPMask, iNumTarget, aTargets);
|
|
}
|
|
|
|
public static void c2s_CmdCastInstantSkill(int idSkill, byte byPVPMask, int iNumTarget, int[] aTargets)
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdCastInstantSkill(idSkill, byPVPMask, iNumTarget, aTargets);
|
|
}
|
|
|
|
public static void c2s_CmdCastPosSkill(int idSkill, Vector3 vDest, byte byPVPMask, int iNumTarget, int aTargets)
|
|
{
|
|
Instance._gameSession.c2s_CmdCastPosSkill(idSkill, vDest, byPVPMask, iNumTarget, aTargets);
|
|
}
|
|
public static async Task Login(string username, string password, Action<bool> onLoginComplete = null)
|
|
{
|
|
Instance._username = username;
|
|
Instance._password = password;
|
|
|
|
if (Instance._ip == "" || Instance._port == 0)
|
|
{
|
|
BMLogger.LogError($"IP or port is not set {Instance._ip} {Instance._port}");
|
|
onLoginComplete?.Invoke(false);
|
|
return;
|
|
}
|
|
|
|
BMLogger.Log( $"Connecting to {Instance._ip} {Instance._port}...");
|
|
await Instance.ConnectAsync(Instance._ip, Instance._port);
|
|
|
|
if (!Instance._gameSession.IsConnected)
|
|
{
|
|
BMLogger.LogError($"Failed to connect to {Instance._ip} {Instance._port}");
|
|
onLoginComplete?.Invoke(false);
|
|
return;
|
|
}
|
|
|
|
// New login session: allow outgoing again.
|
|
Instance.SetSuppressOutgoing(false);
|
|
Instance._gameSession.LoginAsync(username, password, onLoginComplete);
|
|
}
|
|
|
|
private void SetSuppressOutgoing(bool suppressed)
|
|
{
|
|
_suppressOutgoing = suppressed;
|
|
try
|
|
{
|
|
_gameSession?.SetGameplayTrafficSuppressed(suppressed, clearCachedCmds: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BMLogger.LogError($"SetSuppressOutgoing failed: {ex.Message}");
|
|
}
|
|
}
|
|
public void c2s_SendCmdStopMove(in Vector3 vDest, float fSpeed, int iMoveMode,
|
|
byte byDir, ushort wStamp, int iTime)
|
|
{
|
|
BMLogger.LogWarning("HoangDev : c2s_SendCmdStopMove");
|
|
Instance._gameSession.c2s_SendCmdStopMove(vDest, fSpeed, iMoveMode, byDir, wStamp, iTime);
|
|
}
|
|
public void c2s_CmdPlayerMove(in Vector3 vCurPos, in Vector3 vDest,
|
|
int iTime, float fSpeed, int iMoveMode, ushort wStamp)
|
|
{
|
|
Instance._gameSession.c2s_CmdPlayerMove(vCurPos, vDest, iTime, fSpeed, iMoveMode, wStamp);
|
|
}
|
|
protected override void Initialize()
|
|
{
|
|
BaseSecurity.Initizalize();
|
|
ProtocolFactory.RegisterAllProtocols();
|
|
// Type 204 is used for both addfriendrqst (server→client request) and addfriendrqstres (client→server response).
|
|
// Client only receives addfriendrqst; ensure decode uses it so we don't get InvalidCastException.
|
|
Protocol.Register<addfriendrqst>((uint)ProtocolType.RPC_ADDFRIENDRQST);
|
|
_gameSession = new GameSession();
|
|
#if UNITY_EDITOR
|
|
var path = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets"));
|
|
#else
|
|
var path = Application.persistentDataPath;
|
|
#endif
|
|
_gameSession.SetLogPath(Path.Combine(path, "Logs", "GameSession.log"));
|
|
|
|
// Subscribe to unexpected disconnects
|
|
_gameSession.Disconnected += OnUnexpectedDisconnect;
|
|
_gameSession.FriendRequestReceived += OnFriendRequestReceived;
|
|
_gameSession.AddFriendResultReceived += OnAddFriendResultReceived;
|
|
|
|
_isInitialized = true;
|
|
|
|
DontDestroyOnLoad(gameObject);
|
|
}
|
|
|
|
private async Task LogoutAndReturnAsync(int outType, LogoutFlowState.LoginEntryTarget entryTarget, bool clearSavedCreds)
|
|
{
|
|
// Tell LoginScene what to show next.
|
|
LogoutFlowState.NextLoginEntry = entryTarget;
|
|
|
|
// Immediately suppress outgoing protocols so background systems can't send after LOGOUT begins.
|
|
SetSuppressOutgoing(true);
|
|
_gameSession.Disconnected -= OnUnexpectedDisconnect;
|
|
_gameSession.FriendRequestReceived -= OnFriendRequestReceived;
|
|
_gameSession.AddFriendResultReceived -= OnAddFriendResultReceived;
|
|
EC_ManMessageMono.Instance.CECNPCMan.Release();
|
|
|
|
if (clearSavedCreds)
|
|
{
|
|
PlayerPrefs.DeleteKey("username");
|
|
PlayerPrefs.DeleteKey("password");
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
try
|
|
{
|
|
if (_gameSession != null && _gameSession.IsConnected)
|
|
{
|
|
// Mark this as an intentional disconnect to prevent showing error message
|
|
_isIntentionalDisconnect = true;
|
|
|
|
// We call after receive LOGOUT(0) or LOGOUT(1) from server, but the server may choose to disconnect immediately or not.
|
|
// Send LOGOUT(outType) like the original client.
|
|
// _gameSession.SendPlayerLogout(outType);
|
|
|
|
// Wait briefly for server-driven disconnect.
|
|
if(outType == 0) await WaitForDisconnectAsync(timeoutMs: 100);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BMLogger.LogError($"LogoutAndReturnAsync exception: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
// Fallback: if server didn't close, close locally.
|
|
if (_gameSession != null && _gameSession.IsConnected && outType == 0)
|
|
{
|
|
_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.
|
|
CleanRuntimeObjects();
|
|
await EnsureLoginSceneAdditiveAndActivateAsync();
|
|
|
|
// 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);
|
|
|
|
if (entryTarget == LogoutFlowState.LoginEntryTarget.LoginUI)
|
|
{
|
|
// Account logout only; half logout keeps SDK credentials so LoginScene can reopen role select.
|
|
if (Tech3CSDKWrapper.Instance.EnsureInitialized())
|
|
{
|
|
Tech3CSDKWrapper.Instance.Logout();
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (and potential duplicate symbol ambiguity across merges):
|
|
// dispatch by name, do not statically bind here.
|
|
ui.SendMessage("ApplyLoginEntry", entryTarget, SendMessageOptions.DontRequireReceiver);
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BMLogger.LogError($"ApplyLoginEntryToUI error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static void CleanRuntimeObjects()
|
|
{
|
|
// 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()
|
|
{
|
|
// get current active scene
|
|
var currentScene = SceneManager.GetActiveScene();
|
|
if ( currentScene.IsValid() && currentScene.name == LoginSceneName)
|
|
{
|
|
// LoginScene is already active, nothing to do.
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// unload the world scene
|
|
SceneManager.UnloadSceneAsync(currentScene);
|
|
}
|
|
|
|
public bool ConsumeUnexpectedDisconnect()
|
|
{
|
|
bool value = _wasUnexpectedlyDisconnected;
|
|
_wasUnexpectedlyDisconnected = false;
|
|
return value;
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
public string GetWorldInstanceName()
|
|
{
|
|
switch (_gameSession.GetRoleInfo().worldtag)
|
|
{
|
|
case 161:
|
|
return "a61";
|
|
case 169:
|
|
return "a69";
|
|
default:
|
|
return "a61";
|
|
}
|
|
}
|
|
|
|
public string GetWorldInstanceName(int idInst)
|
|
{
|
|
switch (idInst)
|
|
{
|
|
case 161:
|
|
return "a61";
|
|
case 169:
|
|
return "a69";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
/// <summary>Make sure username and password is set before calling this method</summary>
|
|
private async Task ConnectAsync(string ip, int port)
|
|
{
|
|
if (!Instance._isInitialized)
|
|
{
|
|
BMLogger.LogError("GameSession is not initialized");
|
|
return;
|
|
}
|
|
await Instance._gameSession.ConnectAsync(ip, port);
|
|
}
|
|
|
|
/// <summary>Get the list of created characters</summary>
|
|
public static void GetRoleListAsync(Action<List<RoleInfo>> callback = null)
|
|
{
|
|
Instance._gameSession.GetRoleListAsync(callback);
|
|
}
|
|
|
|
public static void SelectRoleAsync(RoleInfo roleInfo, Action<RoleInfo> callback = null)
|
|
{
|
|
Instance._gameSession.SelectRoleAsync(roleInfo, callback);
|
|
}
|
|
|
|
public static void CreateRoleAsync(RoleInfo roleInfo, Octets referId, Action<RoleInfo> callback = null)
|
|
{
|
|
Instance._gameSession.CreateRoleAsync(roleInfo, referId, callback);
|
|
}
|
|
|
|
public static void EnterWorldAsync(RoleInfo roleInfo, Action callback = null)
|
|
{
|
|
BMLogger.Log("EnterWorldAsync !!!!! nay ");
|
|
// We are about to re-enter world gameplay: re-enable outgoing gameplay traffic + CmdCache tick.
|
|
// Without this, ReturnToSelectRole flow leaves suppression on and the player can't play after re-enter.
|
|
Instance.SetSuppressOutgoing(false);
|
|
Instance._gameSession.EnterWorldAsync(roleInfo, callback);
|
|
}
|
|
public static void SendChatData(byte cChannel, in string szMsg, int iPack, int iSlot)
|
|
{
|
|
|
|
Instance._gameSession.SendChatData(cChannel, szMsg, iPack, iSlot);
|
|
}
|
|
public static void RequestInventoryAsync(byte byPackage, Action callback = null)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdGetIvtrDetailData(byPackage, callback);
|
|
}
|
|
public static void RequesrQueryPlayerCash()
|
|
{
|
|
if(Instance == null) return;
|
|
if(Instance._gameSession == null ) return;
|
|
Instance._gameSession.c2s_SendCmdQueryCashInfo();
|
|
}
|
|
public static void RequestDropEquipItem(byte index)
|
|
{
|
|
Instance._gameSession.RequestDropEquipItem(index);
|
|
}
|
|
public static void RequestEquipItemAsync(byte iIvtrIdx, byte iEquipIdx, Action callback = null)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdEquipItem(iIvtrIdx, iEquipIdx, callback);
|
|
}
|
|
public static void RequestPickupItem(int idItem, int tid)
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdPickUp(idItem, tid);
|
|
}
|
|
public static void RequestDropIvrtItem(byte index, int amount)
|
|
{
|
|
Instance._gameSession.RequestDropIvtrItem(index, amount);
|
|
}
|
|
|
|
public static void RequestExchangeIvtrItem(byte index1, byte index2)
|
|
{
|
|
Instance._gameSession.RequestExchangeIvtrItem(index1, index2);
|
|
}
|
|
|
|
public static void RequestMoveIvtrItem(byte src, byte dest, uint count)
|
|
{
|
|
Instance._gameSession.RequestMoveIvtrItem(src, dest, count);
|
|
}
|
|
public static void LoadConfigData()
|
|
{
|
|
Instance._gameSession.LoadConfigData();
|
|
}
|
|
public static void RequestCheckSecurityPassWd(string password)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdOpenFashionTrash(password);
|
|
}
|
|
public static void c2s_SendCmdContinueAction()
|
|
{
|
|
Instance._gameSession.c2s_SendCmdContinueAction();
|
|
}
|
|
public static void c2s_CmdReviveVillage()
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdReviveVillage();
|
|
}
|
|
public static void c2s_CmdReviveItem()
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdReviveItem();
|
|
}
|
|
public static void RequestReviveByPlayer()
|
|
{
|
|
Instance._gameSession.RequestReviveByPlayer();
|
|
}
|
|
public static void c2s_SendCmdUseItemWithTarget(byte byPackage, byte bySlot, int tid, byte byPVPMask)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdUseItemWithTarget(byPackage, bySlot, tid, byPVPMask);
|
|
}
|
|
|
|
public void RequestMallShopping(uint count, int good_id, int good_index, int good_pos)
|
|
{
|
|
var goods = new CMD_MallShopping.goods[]
|
|
{
|
|
new CMD_MallShopping.goods
|
|
{
|
|
goods_id = good_id,
|
|
goods_index = good_index,
|
|
goods_pos = good_pos
|
|
}
|
|
};
|
|
Instance._gameSession.c2s_SendCmdMallShopping(count, goods);
|
|
}
|
|
public static void RequestAllInventoriesAsync(Action callback = null, params byte[] packages)
|
|
{
|
|
if (packages == null || packages.Length == 0)
|
|
{
|
|
packages = new byte[] { 0, 1, 2 };
|
|
}
|
|
|
|
int remaining = packages.Length;
|
|
Action onOneDone = () =>
|
|
{
|
|
remaining--;
|
|
if (remaining <= 0)
|
|
{
|
|
callback?.Invoke();
|
|
}
|
|
};
|
|
|
|
foreach (var p in packages)
|
|
{
|
|
RequestInventoryAsync(p, onOneDone);
|
|
}
|
|
}
|
|
public static void c2s_SendCmdNPCSevLearnSkill(int idSkill)
|
|
{
|
|
// BMLogger.LogError("c2s_SendCmdNPCSevLearnSkill");
|
|
Instance._gameSession.c2s_SendCmdNPCSevLearnSkill(idSkill);
|
|
}
|
|
public static void c2s_CmdNPCSevHello(int nid)
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdNPCSevHello(nid);
|
|
}
|
|
|
|
public static void c2s_CmdNormalAttack(byte byPVPMask)
|
|
{
|
|
Instance._gameSession.c2s_CmdNormalAttack(byPVPMask);
|
|
}
|
|
|
|
public static void c2s_CmdCancelAction()
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdCancelAction();
|
|
}
|
|
|
|
public static void c2s_CmdUnselect()
|
|
{
|
|
Instance._gameSession.c2s_CmdUnselect();
|
|
}
|
|
|
|
public static void c2s_CmdSelectTarget(int idTarget)
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdSelectTarget(idTarget);
|
|
}
|
|
public static void c2s_CmdNPCSevWaypoint()
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevWaypoint();
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevMakeItem(int idSkill, int idItem, uint dwCount)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevMakeItem(idSkill, idItem, dwCount);
|
|
}
|
|
|
|
public void GetFactionInfo(int iNumFaction, int[] aFactinoIDs)
|
|
{
|
|
m_stubbornFactionInfoSender.Add(iNumFaction, aFactinoIDs);
|
|
}
|
|
public static void c2s_CmdSendEnterPKPrecinct()
|
|
{
|
|
Instance._gameSession.c2s_CmdSendEnterPKPrecinctint();
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevHeal()
|
|
{
|
|
|
|
}
|
|
public static void c2s_SendCmdNotifyForceAttack(int iForceAttack, byte refuseBless)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNotifyForceAttack(iForceAttack, refuseBless);
|
|
}
|
|
public static void c2s_CmdNPCSevAcceptTask(int idTask,int idStorage,int idRefreshItem)
|
|
{
|
|
Instance._gameSession.c2s_CmdNPCSevAcceptTask(idTask, idStorage, idRefreshItem);
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevReturnTask(int idTask, int iChoice)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevReturnTask(idTask, iChoice);
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevTaskMatter(int idTask)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevTaskMatter(idTask);
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevBuy(int itemNum, npc_trade_item[] items)
|
|
{
|
|
if (items == null || itemNum <= 0)
|
|
return;
|
|
Instance._gameSession.c2s_SendCmdNPCSevBuy(itemNum, items);
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevSell(int itemNum, npc_sell_item[] items)
|
|
{
|
|
if (items == null || itemNum <= 0)
|
|
return;
|
|
Instance._gameSession.c2s_SendCmdNPCSevSell(itemNum, items);
|
|
}
|
|
public static void c2s_CmdStandUp()
|
|
{
|
|
Instance._gameSession.c2s_SendCmdStandUp();
|
|
}
|
|
#region Task
|
|
public static void c2s_CmdGetAllData(bool byPack, bool byEquip, bool byTask)
|
|
{
|
|
//Debug.Log("[Dat]- SendCmdGetAllData");
|
|
Instance._gameSession.c2s_SendCmdGetAllData(byPack, byEquip, byTask);
|
|
}
|
|
|
|
public static void c2s_CmdEmoteAction(uint wPose)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdEmoteAction(wPose);
|
|
}
|
|
public static void c2s_CmdTaskNotify( byte[] pBuf, uint sz)
|
|
{
|
|
if (Instance != null && Instance._gameSession != null)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTaskNotify( pBuf, sz);
|
|
}
|
|
}
|
|
|
|
public static void c2s_CmdAutoTeamSetGoal(int type, int goal_id, int op)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdAutoTeamSetGoal(type, goal_id, op);//{ ::c2s_SendCmdAutoTeamSetGoal(type, goal_id, op); }
|
|
}
|
|
|
|
public static void c2s_CmdTeamInvite(int idPlayer)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamInvite(idPlayer);
|
|
}
|
|
public static void c2s_CmdTeamAgreeInvite(int idLeader, int team_seq)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamAgreeInvite(idLeader, team_seq);
|
|
}
|
|
public static void c2s_CmdTeamRejectInvite(int idLeader)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamRejectInvite(idLeader);
|
|
}
|
|
public static void c2s_CmdDuelRequest(int idTarget)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdDuelRequest(idTarget);
|
|
}
|
|
public static void c2s_CmdDuelReply(bool accept, int idInviter)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdDuelReply(accept, idInviter);
|
|
}
|
|
/// <summary>Send PROTOCOL_ADDFRIEND(202). Port of CECGameSession::friend_Add.</summary>
|
|
public static void Friend_Add(int idPlayer, string name)
|
|
{
|
|
Instance._gameSession.Friend_Add(idPlayer, name ?? "");
|
|
}
|
|
/// <summary>Send PROTOCOL_GETFRIENDS(206). Port of CECGameSession::friend_GetList().</summary>
|
|
public static void Friend_GetList()
|
|
{
|
|
Instance._gameSession.Friend_GetList();
|
|
}
|
|
public static void Friend_AddResponse(uint xid, bool agree)
|
|
{
|
|
Instance._gameSession.Friend_AddResponse(xid, agree);
|
|
}
|
|
public static void c2s_CmdTeamKickMember(int idMember)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamKickMember(idMember);
|
|
}
|
|
public static void c2s_CmdTeamLeaveParty()
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamLeaveParty();
|
|
}
|
|
public static void c2s_CmdTeamDismissParty()
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamDismissParty();
|
|
}
|
|
public static void c2s_CmdTeamSetPickupFlag(short pickupFlag)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamSetPickupFlag(pickupFlag);
|
|
}
|
|
public static void c2s_CmdTeamChangeLeader(int idNewLeader)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamChangeLeader(idNewLeader);
|
|
}
|
|
public static void c2s_CmdTeamMemberPos(int count, int[] memberIds)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdTeamMemberPos(count, memberIds);
|
|
}
|
|
|
|
public static void c2s_CmdGatherMaterial(int idMatter, int iToolPack, int idToolIndex, int idTool, int idTask)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdGatherMaterial(idMatter, iToolPack, idToolIndex, idTool, idTask);
|
|
}
|
|
#endregion
|
|
|
|
public static void GetRoleBaseInfo(int iNumRole, List<int> aRoleIDs)
|
|
{
|
|
Instance._gameSession.GetRoleBaseInfo(iNumRole, aRoleIDs);
|
|
}
|
|
|
|
public static void c2s_CmdGetOtherEquip(int iNumRole, List<int> aRoleIDs)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdGetOtherEquip(iNumRole, aRoleIDs);
|
|
}
|
|
|
|
public static void GetRoleCustomizeData(int iNumRole, List<int> aRoleIDs)
|
|
{
|
|
Instance._gameSession.GetRoleCustomizeData(iNumRole, aRoleIDs);
|
|
}
|
|
|
|
public void LoadScene(string sceneName, LoadSceneMode mode, Action<float> actProgress, Action<bool> actDone)
|
|
{
|
|
// SceneLoadService.Load(sceneName, mode, actDone);
|
|
StartCoroutine(LoadSceneCoroutine(sceneName, mode, actProgress, actDone));
|
|
}
|
|
|
|
private IEnumerator LoadSceneCoroutine(string sceneName, LoadSceneMode mode, Action<float> actProgress, Action<bool> actDone)
|
|
{
|
|
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, mode);
|
|
asyncLoad.allowSceneActivation = false;
|
|
while (!asyncLoad.isDone)
|
|
{
|
|
if (asyncLoad.progress >= 0.9f)
|
|
{
|
|
asyncLoad.allowSceneActivation = true;
|
|
}
|
|
yield return null;
|
|
}
|
|
actDone?.Invoke(true);
|
|
}
|
|
|
|
new void OnDestroy()
|
|
{
|
|
// Mark as intentional to prevent showing disconnect message
|
|
_isIntentionalDisconnect = true;
|
|
|
|
_gameSession.Disconnect();
|
|
_gameSession.Dispose();
|
|
CECNPC.ReleaseStaticRes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles unexpected server disconnections. Shows a message box and returns to login.
|
|
/// </summary>
|
|
private void OnFriendRequestReceived(uint xid, int srcroleid, string askerName)
|
|
{
|
|
string name = string.IsNullOrEmpty(askerName) ? ("Player " + srcroleid) : askerName;
|
|
CECUIManager.Instance?.ShowMessageBoxYesAndNo(
|
|
title: "Friend Request",
|
|
message: $"{name} muốn hảo hữu với bạn.",
|
|
dlg: null,
|
|
onClickedYes: () => Friend_AddResponse(xid, agree: true),
|
|
onClickedNo: () => Friend_AddResponse(xid, agree: false));
|
|
}
|
|
|
|
private void OnAddFriendResultReceived(byte retcode, string message)
|
|
{
|
|
CECUIManager.Instance?.ShowMessageBoxYes(
|
|
title: retcode == 0 ? "Friend added" : "Đã có trong danh sách bạn bè, không thể hảo hữu",
|
|
message: message,
|
|
dlg: null,
|
|
null);
|
|
}
|
|
|
|
private void OnUnexpectedDisconnect()
|
|
{
|
|
// If this was an intentional disconnect (logout), skip UI
|
|
if (_isIntentionalDisconnect)
|
|
{
|
|
_isIntentionalDisconnect = false;
|
|
return;
|
|
}
|
|
|
|
_wasUnexpectedlyDisconnected = true;
|
|
|
|
// Show disconnect message box
|
|
CECUIManager.Instance?.ShowMessageBoxYes("Disconnected", "Connection to the server has been lost.", null,
|
|
() =>
|
|
{
|
|
// Return to login screen
|
|
LogoutAccount();
|
|
});
|
|
|
|
}
|
|
|
|
public bool TryConsumeUnexpectedDisconnect()
|
|
{
|
|
if(!_wasUnexpectedlyDisconnected)
|
|
return false;
|
|
|
|
_wasUnexpectedlyDisconnected = false;
|
|
return true;
|
|
}
|
|
|
|
public static void c2s_CmdGoto(float x, float y, float z)
|
|
{
|
|
Instance._gameSession.c2s_CmdGoto(x, y, z);
|
|
}
|
|
|
|
public static void c2s_SendCmdUseItem(byte byPackage, byte bySlot, int tid, byte byCount)
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdUseItem(byPackage, bySlot, tid, byCount);
|
|
}
|
|
|
|
// Send C2S::GET_EXT_PROP commadn data
|
|
public static void c2s_SendCmdGetExtProps()
|
|
{
|
|
Instance._gameSession.CmdCache.SendCmdExtProps();
|
|
}
|
|
|
|
/// <summary>Request other player profile/equip detail (C2S GET_OTHER_EQUIP_DETAIL). Server responds with PLAYER_EQUIP_DETAIL; response is currently logged via Debug.Log.</summary>
|
|
public static void c2s_SendCmdGetOtherEquipDetail(int roleId)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdGetOtherEquipDetail(roleId);
|
|
}
|
|
|
|
/// <summary>Send C2S::SET_STATUS_POINT (attribute point allocation).</summary>
|
|
public static void c2s_CmdSetStatusPts(int vitality, int energy, int strength, int agility)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdSetStatusPts(vitality, energy, strength, agility);
|
|
}
|
|
|
|
public static void c2s_SendCmdGivePresent(int roleid, int mail_id, int goods_id, int goods_index, int goods_slot)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdGivePresent(roleid, mail_id, goods_id, goods_index, goods_slot);
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
// Don't tick/schedule outgoing C2S while we're in logout/role-select flow.
|
|
if (!_suppressOutgoing)
|
|
_gameSession?.CmdCache?.Tick(Time.deltaTime);
|
|
|
|
#if UNITY_EDITOR
|
|
// Debug: Press D to disconnect from server (Editor only)
|
|
/* if (Input.GetKeyDown(KeyCode.D))
|
|
{
|
|
if (_gameSession != null && _gameSession.IsConnected)
|
|
{
|
|
Debug.Log("[DEBUG] D key pressed - Force disconnecting from server...");
|
|
_gameSession.Disconnect();
|
|
}
|
|
}*/
|
|
#endif
|
|
}
|
|
|
|
public static void c2s_CmdPetCtrl(int idTarget, int cmd, object pParamBuf, int iParamLen)
|
|
{
|
|
Instance._gameSession.SendCmdPetCtrl(idTarget, cmd, pParamBuf, iParamLen);
|
|
}
|
|
|
|
// Pet commands ...
|
|
public static void c2s_CmdPetSummon(int iPetIdx)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdPetSummon(iPetIdx);
|
|
}
|
|
|
|
public static void c2s_CmdNPCSevEmbed(ushort wStoneIdx, ushort wEquipIdx, int tidStone, int tidEquip)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevEmbed(wStoneIdx, wEquipIdx, tidStone, tidEquip);
|
|
}
|
|
public static void c2s_CmdGetItemInfo(byte byPackage, byte bySlot)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdGetItemInfo(byPackage, bySlot);
|
|
}
|
|
public static void c2s_CmdNPCSevClearEmbeddedChip(int iEquipIdx, int tidEquip)
|
|
{
|
|
Instance._gameSession.c2s_CmdNPCSevClearEmbeddedChip(iEquipIdx, tidEquip);
|
|
}
|
|
public static void c2s_CmdPetRecall()
|
|
{
|
|
Instance._gameSession.c2s_SendCmdPetRecall();
|
|
}
|
|
public static void c2s_CmdDebug(ushort icmd, int param1 = int.MinValue)
|
|
{
|
|
Instance._gameSession.c2s_CmdDebug(icmd, param1);
|
|
}
|
|
public static void c2s_CmdNPCSevHatchPet(int iIvtrIdx, int idEgg)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevHatchPet(iIvtrIdx, idEgg);
|
|
}
|
|
public static void c2s_CmdNPCSevRestorePet(int iPetIdx)
|
|
{
|
|
Instance._gameSession.c2s_SendCmdNPCSevRestorePet(iPetIdx);
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|