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.Scripts.Task; using BrewMonster.UI; using UnityEngine; using UnityEngine.SceneManagement; using BrewMonster.Scripts; namespace BrewMonster.Network { // How to connect to the server: // 1. Set the connection info // 2. Login public class UnityGameSession : MonoSingleton { 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; 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; base.Awake(); } private void Start() { CECNPC.InitStaticRes(); } /// /// Send a /// /// /// public static void SendProtocol(Protocol protocol, Action complete = null) { if (!Instance._isInitialized) { return; } Instance._gameSession.SendProtocol(protocol, complete); } /// Set the connection info. This MUST be called call before login public static void SetConnectionInfo(string ip, int port) { BMLogger.Log($"Set connection info {ip} {port}"); Instance._ip = ip; Instance._port = port; } /// /// 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. /// 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); } /// /// Origin-like: Account logout from Select Role (LOGOUT(0)). /// This will clear saved creds and load LoginScene showing username/password UI. /// 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); } 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 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; } Instance._gameSession.LoginAsync(username, password, onLoginComplete); } 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(); _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; _isInitialized = true; 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) { // Mark this as an intentional disconnect to prevent showing error message _isIntentionalDisconnect = true; // 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); // now we have to logout from Tech3C SDK 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(); 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(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"; default: return "a61"; } } /// Make sure username and password is set before calling this method private async Task ConnectAsync(string ip, int port) { if (!Instance._isInitialized) { BMLogger.LogError("GameSession is not initialized"); return; } await Instance._gameSession.ConnectAsync(ip, port); } /// Get the list of created characters public static void GetRoleListAsync(Action> callback = null) { Instance._gameSession.GetRoleListAsync(callback); } public static void SelectRoleAsync(RoleInfo roleInfo, Action callback = null) { Instance._gameSession.SelectRoleAsync(roleInfo, callback); } public static void CreateRoleAsync(RoleInfo roleInfo, Octets referId, Action callback = null) { Instance._gameSession.CreateRoleAsync(roleInfo, referId, callback); } public static void EnterWorldAsync(RoleInfo roleInfo, Action callback = null) { BMLogger.Log("EnterWorldAsync !!!!! nay "); 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() { 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 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_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 aRoleIDs) { Instance._gameSession.GetRoleBaseInfo(iNumRole, aRoleIDs); } public static void GetRoleCustomizeData(int iNumRole, List aRoleIDs) { Instance._gameSession.GetRoleCustomizeData(iNumRole, aRoleIDs); } public void LoadScene(string sceneName, LoadSceneMode mode, Action actProgress, Action actDone) { // SceneLoadService.Load(sceneName, mode, actDone); StartCoroutine(LoadSceneCoroutine(sceneName, mode, actProgress, actDone)); } private IEnumerator LoadSceneCoroutine(string sceneName, LoadSceneMode mode, Action actProgress, Action 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); } void OnDestroy() { // Mark as intentional to prevent showing disconnect message _isIntentionalDisconnect = true; _gameSession.Disconnect(); _gameSession.Dispose(); CECNPC.ReleaseStaticRes(); } /// /// Handles unexpected server disconnections. Shows a message box and returns to login. /// private void OnUnexpectedDisconnect() { // If this was an intentional disconnect (logout), skip UI if (_isIntentionalDisconnect) { _isIntentionalDisconnect = false; return; } // Show disconnect message box CECUIManager.Instance?.ShowMessageBox( title: "Disconnected", message: "Connection to the server has been lost.", messageBoxType: MessageBoxType.YesButton, onClickedYes: () => { // Return to login screen LogoutAccount(); } ); } 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(); } /// Send C2S::SET_STATUS_POINT (attribute point allocation). 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() { _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); } } /// /// 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. /// 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; } /// Consume and reset to default (LoginUI). public static LoginEntryTarget ConsumeNextLoginEntry() { var v = _nextEntry; _nextEntry = LoginEntryTarget.LoginUI; return v; } } }