Files
5.24BuYu/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs
T
2026-10-17 03:01:15 +08:00

1458 lines
67 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.WebResponses;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
namespace Assets.MyGame.Scripts.Net
{
public class WebSocketReponseList
{
/// <summary>已加载(或正在加载)的游戏场景名集合,防止人机 RoomJoinNotice 触发重复 LoadSceneAsync</summary>
private static readonly HashSet<string> _loadedGameScenes = new HashSet<string>();
/// <summary>
/// 游戏场景的 Addressables 加载句柄。
/// 游戏场景(XJMH/FLZY/HHWK/YYSC)里挂的是热更脚本(BGManager/BgFit 等),
/// HybridCLR 要求热更脚本所在场景必须通过 AB/AA 加载,否则运行时会变成 missing script。
/// 卸载时也必须用这个句柄释放,不能只调 SceneManager.UnloadSceneAsync(否则 ab 包会泄漏)。
/// </summary>
private static readonly Dictionary<string, AsyncOperationHandle<SceneInstance>> _gameSceneHandles
= new Dictionary<string, AsyncOperationHandle<SceneInstance>>();
/// <summary>清除指定场景的加载标记(退出游戏时调用,下次进入可重新加载)</summary>
public static void ClearLoadedGameScene(string sceneName)
{
if (!string.IsNullOrEmpty(sceneName))
_loadedGameScenes.Remove(sceneName);
}
/// <summary>
/// 获取房间列表 room
/// </summary>
/// <param name="msg"></param>
public static void GetRoomListChineseChessResult(string msg)
{
var var1 = UIManager.Instance.Show<UIChineseChessDistrict>(UIPanel.MainGamePanelZGXQ, msg);
}
public static void GetGoldConfigSuccessChineseChessResult(string msg)
{
UIManager.Instance.Get<ChineseChessMainPanel>(UIPanel.MainGamePanelZGXQ).subUIManager.ShowAsync(UIPanel.ZGXQGameExplain);
}
/// <summary>
/// 获取房间列表 room
/// </summary>
/// <param name="msg"></param>
public static void GetRoomListResult(string msg)
{
var var1 = UIManager.Instance.Get<UIHomeMain>(UIPanel.HomeMainPanel);
var selectRoomPanel = var1.subUIManager.Get<UIHome_BY_SelectRoomLevelPanel>(UIPanel.SelectRoomLeavePenel);
if (selectRoomPanel != null && selectRoomPanel.isMySelectRoom)
{
// 手动选座:显示房间列表给玩家选
selectRoomPanel.panel_UIHome_BY_MySelectRoomPenel.Show(msg);
}
// 快速加入分支已迁移到 OnSelectRoom → /api/Room/QuickJoin,不再走 GetRoomList
}
/// <summary>
/// 加入房间 room
/// </summary>
/// <param name="msg"></param>
public static void RoomJoinNotice(string msg)
{
// ⭐ 只反序列化一次复用(金币流水 / currRoomData.players 补齐 / 座位激活)
RoomJoinNotice notice = null;
try
{
notice = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomJoinNotice>>(msg).arguments[0];
}
catch { }
// ⭐ 记录金币流水:收到加钱信息(RoomJoinNotice 推送,尚未执行)—— 只记录自己
try
{
string myId = GameManager.Instance.user?.id;
if (notice != null && !string.IsNullOrEmpty(myId) && notice.UserId == myId)
{
GameManager.LogGoldFlowReceiveGain("RoomJoinNotice", $"推送金币={notice.Gold} 钻石={notice.Diamond} 炮台消耗={notice.CannonMultiple} 座位={notice.Position}");
}
}
catch { }
// ⭐ 修复:把加入玩家 upsert 进 currRoomData.players。
// JoinGame 里 SetUserInfo 按 userId 从 currRoomData.players 查显示信息
// (昵称/头像/VIP/短编号),而 currRoomData 只在本地进房收到 RoomInfo 时写一次,
// 后加入的玩家不在快照里 → 老客户端把他显示成匿名(无昵称/默认头像/原始32位长ID)。
// RoomJoinNotice 自带完整玩家数据,这里补进快照后再走 JoinGame。
UpsertPlayerToCurrRoom(notice);
string sceneName = "";
UIPanel uIPanel = UIPanel.None;
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.JQQD:
break;
case GameType.defaultPanel:
break;
case GameType.FLZY:
sceneName = "FLZY";
uIPanel = UIPanel.MainGamePanelFLZY;
break;
case GameType.YYSC:
sceneName = "YYSC";
uIPanel = UIPanel.MainGamePanelYYSC;
break;
case GameType.ZGXQ:
break;
case GameType.XJMH:
sceneName = "XJMH";
uIPanel = UIPanel.MainGamePanelXJMH;
break;
case GameType.HHWK:
sceneName = "HHWK";
uIPanel = UIPanel.MainGamePanelHHWK;
break;
default:
break;
}
if (UIManager.Instance.Get<GameMainPanel_Base>(uIPanel)==null)
{
// ① 先卸载其他已加载的游戏地图场景(保证场景唯一性),不卸载即将加载的目标场景
string[] gameScenes = { "FLZY", "YYSC", "XJMH", "HHWK" };
foreach (var sn in gameScenes)
{
if (sn == sceneName) continue;
var s = SceneManager.GetSceneByName(sn);
if (s.IsValid() && s.isLoaded)
{
UnloadGameScene(sn);
}
}
// ② 先显示游戏面板(加载页面,含进度条):ShowAsync 的 onShown 回调在面板实例化并显示后触发,
// 用它保证"面板先显示 → 再加载场景 → 再加载资源"的严格先后顺序。
// 全程为协程异步,不做 WaitForCompletion 之类的同步等待,不阻塞主线程。
UIManager.Instance.ShowAsync(uIPanel, UIPanel.HomeMainPanel, msg, (panel) =>
{
UIManager.Instance.StartCoroutine(LoadSceneThenResources(panel, sceneName));
});
WebSocketManager.Instance.isGame = true;
}
else
{
// 面板已存在 → 新玩家加入,转发给现有面板激活座位
var panel = UIManager.Instance.Get<GameMainPanel_Base>(uIPanel);
if (panel is GameMainPanel_FishBase fishPanel && notice != null)
{
// 如果座位被bot占用则替换
// ⭐ 座位镜像:服务端座位转显示座位再找场景座位(JoinGame 内部也做同样转换)
var targetSit = fishPanel.list_Sits.Find(s => s.posID == SeatMirror.ToDisplay(notice.Position));
if (targetSit != null && targetSit.isBot)
{
targetSit.isBot = false;
targetSit.ShowWaiting();
}
fishPanel.JoinGame(notice.Position, notice.Gold, notice.Diamond, notice.CannonMultiple, 0, notice.UserId, notice.CannonId, notice.WingsId, notice.WarGodId);
// 后端动态加入的机器人也要标记
if (targetSit != null && !string.IsNullOrEmpty(notice.UserId) && notice.UserId.StartsWith("bot_"))
targetSit.isBot = true;
}
}
}
/// <summary>
/// ③ 加载游戏场景(异步等待完成)→ ④ 场景就绪后再让面板加载资源(鱼/音频/路线等)。
/// 全程逐帧 yield 等待,不做同步等待,不阻塞主线程。
/// </summary>
private static IEnumerator LoadSceneThenResources(UIBase panel, string sceneName)
{
// ⭐ 第一步只加载 UI:面板刚实例化显示,先让 2 帧把 UI(含进度条)完整渲染出来,
// 再从"里面有进度条的画面"开始加载非 UI 资源(场景/鱼/音频)——
// 避免面板第一帧和场景加载启动挤在同一帧,UI 出现那下发卡
yield return null;
yield return null;
// ⭐ 场景加载阶段进度条从 0 缓慢爬升(此阶段只加载了 UI 面板):条不会静止像卡死
if (panel is GameMainPanel_FishBase barPanel)
barPanel.BeginSceneLoadingProgress();
// ③ 目标场景仅加载一次:标记已加载后不再重复 LoadSceneAsync(防止人机通知触发重复加载)
if (!string.IsNullOrEmpty(sceneName) && !_loadedGameScenes.Contains(sceneName))
{
_loadedGameScenes.Add(sceneName);
var targetScene = SceneManager.GetSceneByName(sceneName);
if (!targetScene.IsValid() || !targetScene.isLoaded)
{
// ⭐ 必须通过 Addressables 加载:场景里挂的是热更脚本(Game.Hotfix 里的 BGManager/BgFit 等),
// 如果走 Build Settings(随主包)加载,热更脚本会解析失败变成 missing script —— 背景就会不动。
var handle = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
_gameSceneHandles[sceneName] = handle;
while (!handle.IsDone) yield return null;
if (handle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[GameScene] Addressables 加载场景失败: {sceneName} → {handle.OperationException}。" +
$"请确认已在 Addressables 里把该场景标记为可寻址,且 address = \"{sceneName}\"。");
_loadedGameScenes.Remove(sceneName);
_gameSceneHandles.Remove(sceneName);
}
}
}
// ④ 场景就绪后再加载面板资源
if (panel is GameMainPanel_FishBase fishPanel)
fishPanel.BeginLoadResources();
}
/// <summary>
/// 卸载一个已加载的游戏场景。
/// 如果该场景是通过 Addressables 加载的,必须用句柄释放(否则 ab 包会一直驻留);
/// 没有句柄时退回 SceneManager 卸载,兼容历史上用 SceneManager 加载的场景。
/// </summary>
public static void UnloadGameScene(string sceneName)
{
if (_gameSceneHandles.TryGetValue(sceneName, out var handle))
{
if (handle.IsValid())
Addressables.UnloadSceneAsync(handle, true);
_gameSceneHandles.Remove(sceneName);
}
else
{
SceneManager.UnloadSceneAsync(sceneName);
}
_loadedGameScenes.Remove(sceneName);
}
/// <summary>
/// 把 RoomJoinNotice 玩家数据写入 currRoomData.players(存在则更新,不存在则添加)。
/// JoinGame 里 SetUserInfo 按 userId 从 currRoomData.players 查显示信息
/// (昵称/头像/VIP/短编号),后加入的玩家不在快照里会显示成匿名玩家。
/// </summary>
private static void UpsertPlayerToCurrRoom(RoomJoinNotice notice)
{
if (notice == null || string.IsNullOrEmpty(notice.UserId)) return;
var roomData = GameManager.Instance.gameData?.currRoomData;
if (roomData == null) return;
if (roomData.players == null) roomData.players = new List<RoomPlayerInfo>();
var p = roomData.players.Find(x => x.userId == notice.UserId);
if (p == null)
{
p = new RoomPlayerInfo();
roomData.players.Add(p);
}
p.userId = notice.UserId;
p.userName = notice.UserName;
p.userNumber = notice.UserNumber;
p.avatar = notice.Avatar;
p.isRedName = notice.IsRedName;
p.level = notice.Level;
p.exp = notice.Exp;
p.maxExp = notice.MaxExp;
p.vipLevel = notice.VipLevel;
p.cId = notice.CId;
p.gold = notice.Gold;
p.diamond = notice.Diamond;
p.position = notice.Position;
p.cannonId = notice.CannonId;
p.wingsId = notice.WingsId;
p.warGodId = notice.WarGodId;
p.cannonMultiple = notice.CannonMultiple;
p.beginCoin = notice.BeginCoin;
p.beginTime = notice.BeginTime;
p.state = notice.State;
p.playerFactor = notice.PlayerFactor;
p.totalRechargeGold = notice.TotalRechargeGold;
p.safeBoxGold = notice.SafeBoxGold;
p.isBot = notice.IsBot;
p.isAfk = notice.IsAfk;
}
/// <summary>
/// 房间列表 room
/// </summary>
/// <param name="msg"></param>
public static void RoomInfo(string msg)
{
WebSocketManager.Instance.isGame = false;
var roomData = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomChangeResponse>>(msg).arguments[0];
if (roomData != null)
{
GameManager.Instance.gameData.currRoomData = roomData;
}
else
{
Debug.LogError("roomdata是空:" + msg);
}
}
public static void RoomKillNotice(string msg)
{
// 离开倒计时期间不触发击杀/爆奖特效
var panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelDefault)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelFLZY)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelYYSC)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelXJMH)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelHHWK);
if (panel != null && panel.IsLeavingCountdown()) return;
CreateFishManager.Instance.Ws_DieFish(msg);
}
public static void RoomFireNotice(string msg)
{
FireCannonSendData fireCannonSendData = JsonConvert.DeserializeObject<WebSocketResponseBase<FireCannonSendData>>(msg).arguments[0];
var syncGameData = fireCannonSendData.extra;
EventCenter.Instance.EventTrigger<FireCannonSendData>(ConstEventString.SyncGameData_FireBullet, fireCannonSendData);
}
public static void SyncPlayerGold(string msg)
{
var gold = JsonConvert.DeserializeObject<WebSocketResponseBase<long>>(msg).arguments[0];
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.JQQD:
break;
case GameType.defaultPanel:
UpdateLocalPlayerGold(UIPanel.MainGamePanelDefault, gold);
break;
case GameType.FLZY:
UpdateLocalPlayerGold(UIPanel.MainGamePanelFLZY, gold);
break;
case GameType.YYSC:
UpdateLocalPlayerGold(UIPanel.MainGamePanelYYSC, gold);
break;
case GameType.ZGXQ:
break;
case GameType.XJMH:
UpdateLocalPlayerGold(UIPanel.MainGamePanelXJMH, gold);
break;
case GameType.HHWK:
UpdateLocalPlayerGold(UIPanel.MainGamePanelHHWK, gold);
break;
default:
break;
}
}
/// <summary>
/// 炮倍变更通知:服务端确认玩家炮倍切换后广播给房间内所有玩家。
/// 客户端收到后才应用新炮倍到 consumeGoldNum 和 UI,避免开火扣费不一致。
/// </summary>
public static void ChangeGunMultipleNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<ChangeGunMultipleNotice>>(msg).arguments[0];
var panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelDefault)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelFLZY)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelYYSC)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelXJMH)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelHHWK);
if (panel == null) return;
panel.ApplyCannonMultiple(data.userId, data.multiple);
}
/// <summary>
/// 后端推送 SyncPlayerGold 时同步本地玩家金币(底层值 + UI 显示)。
/// 之前只更新 goldNum 不刷新 tMP_GoldNum.text,导致 UI 显示和后端 authoritative 值不同步,
/// 后续杀鱼累加基于修正值但 UI 显示旧值,差距越拉越大。
/// </summary>
private static void UpdateLocalPlayerGold(UIPanel panelType, long gold)
{
var panel = UIManager.Instance.Get<GameMainPanel_FishBase>(panelType);
if (panel == null || panel.currSitData == null) return;
long oldGold = panel.currSitData.goldNum;
long diff = gold - oldGold;
// ⭐ 记录金币流水:收到后端推送 SyncPlayerGold(尚未执行)
if (diff > 0)
GameManager.LogGoldFlowReceiveGain("SyncPlayerGold", $"推送金币={gold} 修正前金币={oldGold} 差值=+{diff} 座位={panel.currSitData.posID}");
else if (diff < 0)
GameManager.LogGoldFlowReceiveCost("SyncPlayerGold", $"推送金币={gold} 修正前金币={oldGold} 差值={diff} 座位={panel.currSitData.posID}");
panel.currSitData.goldNum = gold;
panel.currSitData.SetGoldNumText(gold);
// ⭐ 记录金币流水:已执行修正(本地已写入)
if (diff > 0)
GameManager.LogGoldFlowAppliedGain("SyncPlayerGold", $"已修正金币={gold} 修正前金币={oldGold} 增加金币=+{diff} 座位={panel.currSitData.posID}");
else if (diff < 0)
GameManager.LogGoldFlowAppliedCost("SyncPlayerGold", $"已修正金币={gold} 修正前金币={oldGold} 扣除金币={diff} 座位={panel.currSitData.posID}");
}
public static void GetRoomInfoResult(string msg)
{
//GameManager.Instance.StartGame();RoomChangeResponse
var roomData = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomChangeResponse>>(msg).arguments[0];
// ⭐ 记录金币流水:收到加钱信息(RoomInfo 推送玩家金币,尚未执行)—— 只记录自己
if (roomData?.players != null)
{
string myId = GameManager.Instance.user?.id;
foreach (var p in roomData.players)
{
if (string.IsNullOrEmpty(myId) || p.userId != myId) continue;
GameManager.LogGoldFlowReceiveGain("RoomInfo", $"推送金币={p.gold} 钻石={p.diamond} 炮台消耗={p.cannonMultiple} 座位={p.position} 房间={roomData.roomId}");
}
}
UIHome_BY_SelectRoomLevelPanel.StartGame(roomData);
}
public static void SyncGameDataResult(string msg)
{
var syncGameData = JsonConvert.DeserializeObject<WebSocketResponseBase<SyncGameData>>(msg).arguments[0];
switch (syncGameData.frameType)
{
case FrameDataType.none:
break;
case FrameDataType.rotateTurrect:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_Rotate, syncGameData);
break;
case FrameDataType.fireBullet:
// 机器人角度同步:SyncFire_ForBot 发出的 SyncGameData(fireBullet)
// 与 RoomFireNotice(FireCannonSendData) 分开处理,仅用于存储角度到 botLastAngle
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_BotFireAngle, syncGameData);
break;
// createFish=3 已废弃:鱼创建改由服务端 OnFishSpawn 事件下发
case FrameDataType.changeAutoTarget:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeTargetMonster, syncGameData);
break;
case FrameDataType.randomDieFish:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_RendromDieFish, syncGameData);
break;
case FrameDataType.freezeFish:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_FreezeFish, syncGameData);
break;
case FrameDataType.changeTurretState:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_TurretState, syncGameData);
break;
case FrameDataType.changeTurret:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeTurret, syncGameData);
break;
case FrameDataType.changeCannonMultiple:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeCannonMultiple, syncGameData);
break;
case FrameDataType.changeWing:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeWing, syncGameData);
break;
case FrameDataType.changeZhanShen:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ChangeZhanShen, syncGameData);
break;
case FrameDataType.biaoQing:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_BiaoQing, syncGameData);
break;
// requestSyncFish=11 已废弃:鱼同步改由服务端 OnFishSpawn(type=sync) 下发
case FrameDataType.baoJiangEnd:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.BaoJiangEnd, syncGameData);
break;
// ⭐ 钻头蟹/激光蟹释放:击杀者点击(或超时)广播,各端(含击杀者回广播)统一时刻发射钻头/激光
case FrameDataType.zuanTouRelease:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_ZuanTouRelease, syncGameData);
break;
// ⭐ 加金币:刮刮卡等 HTTP 途径获得金币后广播,各客户端给对应座位加金币并刷新显示
case FrameDataType.addGold:
EventCenter.Instance.EventTrigger<SyncGameData>(ConstEventString.SyncGameData_AddGold, syncGameData);
break;
default:
break;
}
}
// ================================================================
// 新协议事件分发 (OnFishSpawn / PlayerAfk / FishFrozen / KillNotice 等)
// ================================================================
/// <summary>
/// 服务端出鱼事件 OnFishSpawn 处理。
/// 破坏性替代旧 createFish 帧同步:服务端统一出鱼,客户端只接收并生成。
/// </summary>
public static void OnFishSpawn(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<OnFishSpawnData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<OnFishSpawnData>(ConstEventString.OnFishSpawn, data);
NetLog.Log("出鱼:", msg);
}
/// <summary>鱼列表刷新</summary>
public static void OnFishList(string msg)
{
var fishList = JsonConvert.DeserializeObject<WebSocketResponseBase<object[]>>(msg).arguments[0];
EventCenter.Instance.EventTrigger<object[]>(ConstEventString.OnFishList, fishList);
}
/// <summary>
/// 冰冻道具(40003)生效事件 FishFrozen。
/// 鱼存活时间延长 15 秒,客户端写死 15s 倒计时。
/// </summary>
public static void FishFrozen(string msg)
{
EventCenter.Instance.EventTrigger(ConstEventString.FishFrozen);
}
/// <summary>
/// FishGameHub TorpedoResultNotice — 鱼雷使用结果(房间广播)。
/// 所有客户端都播爆奖流程(与 Boss 爆奖 RoomKillNotice 全端实例化同惯例):
/// 随机炸死场上小鱼 + 红包分包爆金币 + 分数面板 + 飞回使用者座位结算。
/// </summary>
public static void TorpedoResultNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<TorpedoResultNotice>>(msg).arguments[0];
Debug.Log($"[FishGameHub] 鱼雷结果: userId={data?.userId} prop={data?.propNumber} gold={data?.gold} color={data?.color}");
var panel = CreateFishManager.Instance.currGameType;
if (panel != null)
panel.OnTorpedoResult(data);
}
/// <summary>
/// FishGameHub ErrorMessage — 错误通知兜底
/// (鱼雷等新接口失败时推送:房间不存在/道具配置不存在/用户不存在;老接口走 Exception 错误码)。
/// </summary>
public static void ErrorMessage(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<ErrorMessage>>(msg).arguments[0];
if (!string.IsNullOrEmpty(data?.message))
UIManager.Instance.ShowBoardError(data.message);
}
/// <summary>玩家挂机通知</summary>
public static void PlayerAfkNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<PlayerAfkNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<PlayerAfkNoticeData>(ConstEventString.PlayerAfkNotice, data);
}
/// <summary>玩家取消挂机通知</summary>
public static void PlayerCancelAfkNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<PlayerAfkNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<PlayerAfkNoticeData>(ConstEventString.PlayerCancelAfkNotice, data);
}
/// <summary>
/// 玩家信息同步 OnPlayerInfo(GetPlayerInfo 的新响应,发送给调用者)。
/// 旧事件 SendPlayerInfoResult 只返回 gold;本事件返回完整玩家信息,UI 层通过 EventCenter 消费。
/// </summary>
public static void OnPlayerInfo(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<OnPlayerInfoData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<OnPlayerInfoData>(ConstEventString.OnPlayerInfo, data);
}
/// <summary>
/// 召唤卡召唤通知 SummonCardFishNotice。
/// 某玩家使用召唤卡(40006),服务端已创建 Boss鱼(飞机场)/黄金鱼(其他场次)。
/// 仅用于播放召唤提示/特效/显示召唤玩家,实际鱼对象仍由 OnFishSpawn 创建。
/// </summary>
public static void SummonCardFishNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<SummonCardFishNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<SummonCardFishNoticeData>(ConstEventString.SummonCardFishNotice, data);
NetLog.Log("召唤通知:", msg);
}
/// <summary>
/// 全局击杀公告 KillNotice。
/// 真人击杀/机器人模拟,机器人模拟时额外带 fishName/multiplier/sceneName。
/// </summary>
public static void KillNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<KillNoticeData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<KillNoticeData>(ConstEventString.KillNotice, data);
}
/// <summary>机器人退出房间通知</summary>
public static void BotLeaveRoomNotice(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<BotLeaveRoomNoticeData>>(msg).arguments[0];
if (data == null) return;
WebSocketReponseList.BotLeaveRoom(data.userId);
}
/// <summary>房主断线</summary>
public static void HostDisconnectNotice(string msg)
{
EventCenter.Instance.EventTrigger(ConstEventString.HostDisconnect);
WebSocketReponseList.HostDisconnect();
}
/// <summary>
/// 座位变更 OnSeatChange。
/// 数据: { type, userId, position, showWaiting? }
/// </summary>
public static void OnSeatChange(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
if (data == null) return;
// JSON 字段可能是 long/double/string/bool 等任意类型,统一 ToString()+TryParse 容错
int seatType = data.TryGetValue("type", out var t) && int.TryParse(t?.ToString(), out var st) ? st : 0;
string userId = data.TryGetValue("userId", out var u) ? u?.ToString() : "";
int position = data.TryGetValue("position", out var p) && int.TryParse(p?.ToString(), out var pos) ? pos : -1;
bool showWaiting = data.TryGetValue("showWaiting", out var sw) && (sw is bool b ? b : (sw != null && bool.TryParse(sw.ToString(), out var bv) && bv));
WebSocketReponseList.SeatChange(seatType, userId, position, showWaiting);
}
/// <summary>
/// 房主控制权移交 ChangeControllerUser。
/// 仅维护 SitPosData.roomOwnerSit,不再设置 fangzhu(鱼创建已由服务端统一出鱼)。
/// </summary>
public static void ChangeControllerUser(string msg)
{
var userId = JsonConvert.DeserializeObject<WebSocketResponseBase<string>>(msg).arguments[0];
if (userId == GameManager.Instance.user.id)
{
// 本地玩家成为房主:同步本地房主引用
if (SitPosData.localPlayerSit != null)
SitPosData.roomOwnerSit = SitPosData.localPlayerSit;
}
else if (userId != null && userId.StartsWith("bot_"))
{
// 房主是人机:人机无法真正发包,由本地首个非人机玩家代行房主显示职责
SitPosData.TransferRoomOwner();
}
// 其他真人玩家成为房主时无需本地操作(roomOwnerSit 由 TransferRoomOwner/ShowPlayer 维护)
}
public static void LeaveRoomResult(string msg)
{
//WebSocketManager.Instance.isGame = false;
//GameManager.Instance.StartGame();RoomChangeResponse
var response = JsonConvert.DeserializeObject<WebSocketResponseBase<string>>(msg);
// ⭐ 数据保护:arguments 为空直接返回,避免抛异常
if (response?.arguments == null || response.arguments.Count == 0)
{
#if UNITY_EDITOR
Debug.LogWarning("[LeaveRoomResult] arguments 为空,忽略");
#endif
return;
}
var userid = response.arguments[0];
// ⭐ 该玩家没走完的 not-self"爆奖中"BJEndUI 一并清掉:
// 爆奖中途退房的人不会再发 baoJiangEnd,面板会残留在座位上,
// 下一个坐进该座位的玩家会看到这块旧面板(30 秒安全超时还会对着新玩家弹结算)
BaoJiangNotSelfHandler.RemoveByUser(userid);
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.JQQD:
break;
case GameType.defaultPanel:
break;
case GameType.FLZY:
{
var panel = UIManager.Instance.Get<GameMainPanel_FLZY>(UIPanel.MainGamePanelFLZY);
if (panel == null || panel.currSitData == null) break;
if (panel.currSitData.user == userid)
{
ExitFishingGame(UIPanel.MainGamePanelFLZY);
break;
}
foreach (var item in panel.list_Sits)
{
if (item.user == userid)
{
item.ShowWaiting();
}
}
}
break;
case GameType.YYSC:
{
var panel = UIManager.Instance.Get<GameMainPanel_YYSC>(UIPanel.MainGamePanelYYSC);
if (panel == null || panel.currSitData == null) break;
if (panel.currSitData.user == userid)
{
ExitFishingGame(UIPanel.MainGamePanelYYSC);
break;
}
foreach (var item in panel.list_Sits)
{
if (item.user == userid)
{
item.ShowWaiting();
}
}
}
break;
case GameType.ZGXQ:
break;
case GameType.XJMH:
{
var panel = UIManager.Instance.Get<GameMainPanel_XJMH>(UIPanel.MainGamePanelXJMH);
if (panel == null || panel.currSitData == null) break;
if (panel.currSitData.user == userid)
{
ExitFishingGame(UIPanel.MainGamePanelXJMH);
break;
}
foreach (var item in panel.list_Sits)
{
Debug.Log(item.user + "---" + userid);
if (item.user == userid)
{
Debug.Log("推出星际迷航");
item.ShowWaiting();
}
}
}
break;
case GameType.HHWK:
{
var panel = UIManager.Instance.Get<GameMainPanel_HHWK>(UIPanel.MainGamePanelHHWK);
if (panel == null || panel.currSitData == null) break;
if (panel.currSitData.user == userid)
{
ExitFishingGame(UIPanel.MainGamePanelHHWK);
break;
}
foreach (var item in panel.list_Sits)
{
if (item.user == userid)
{
item.ShowWaiting();
}
}
}
break;
default:
break;
}
}
/// <summary>
/// 统一退出捕鱼游戏流程:
/// 1. 关闭 game socket(自己的离房通知已收到,可以正常关闭)
/// 2. 拉取最新用户数据并触发 OnUserDataChanged 事件,同步大厅金币/钻石/等级等 UI
/// 3. 通知游戏面板 socket 已返回 → 解除进度条 0.9 的等待 → 走完剩余进度后自动切回 home
/// </summary>
private static void ExitFishingGame(UIPanel gamePanel)
{
// 标记游戏已结束
WebSocketManager.Instance.isGame = false;
// ⭐ 记录金币流水:退出房间
GameManager.LogGoldFlow("ExitRoom", $"退出房间 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
// 关闭游戏连接
WebSocketManager.Instance.Close(SocketType.game);
// 拉取最新用户数据,触发 UI 同步更新(金币/钻石/等级/VIP等)
APIRequestList.GetUser((user) =>
{
GameManager.Instance.UpdataUserData(user);
});
// ⭐ 通知面板 socket 已返回 → 进度条从 0.9 走到 1.0 → 切回 home
var panel = UIManager.Instance.Get<GameMainPanel_FishBase>(gamePanel);
if (panel != null)
{
panel.OnLeaveRoomResultReceived(gamePanel);
}
else
{
// 兜底:拿不到面板直接切换
UIManager.Instance.ShowAsync(UIPanel.HomeMainPanel, gamePanel);
}
}
// ---------------- LoginHub ----------------
/// <summary>
/// LoginHub Broadcast — 大厅公告广播
/// 首字符命中大喇叭标识(APIRequestList.BroadcastMarker)时按大喇叭格式渲染:
/// 去掉标识,补"喇叭图标+打码昵称"前缀;否则原样显示为普通公告。
/// </summary>
public static void LoginBroadcast(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<LoginBroadcastData>>(msg).arguments[0];
string message = data?.message;
if (!string.IsNullOrEmpty(message) && message[0] == APIRequestList.BroadcastMarker)
{
// 大喇叭:服务端只存了"标识+正文",昵称从广播数据取,展示时再拼接前缀
string content = message.Substring(1);
string userName = data.userName;
if (string.IsNullOrEmpty(userName)) userName = "玩家";
UIManager.Instance.ShowBoardGongGao($"<sprite index=0>【{MaskBroadcastName(userName)}】:{content}");
}
else
{
// 普通公告:原样显示
UIManager.Instance.ShowBoardGongGao(message);
}
}
/// <summary>
/// 玩家名打码(大喇叭展示格式):保留首尾字符、中间一个 *("小金鱼"→"小*鱼")。
/// 与 GameMainPanel_FishBase / UIHome_BY_BackPackPanel 同格式。
/// </summary>
private static string MaskBroadcastName(string name)
{
if (string.IsNullOrEmpty(name)) return name;
name = name.Trim();
if (name.Length <= 1) return name;
return name[0] + "*" + name[name.Length - 1];
}
/// <summary>
/// LoginHub AssetUpdated — 资产变动通知
/// </summary>
public static void AssetUpdated(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<LoginAssetData>>(msg).arguments[0];
// 刮刮卡面板打开期间挂起推送刷新:金币/钻石/道具统一在点击"领取"按钮时刷新
if (GuaGuaLeGPU.SuspendWsRefresh) return;
// 触发用户数据刷新
var userId = GameManager.Instance?.user?.id;
if (!string.IsNullOrEmpty(userId))
APIRequestList.GetUser(userId, a => GameManager.Instance.UpdataUserData(a));
}
/// <summary>
/// LoginHub BackpackUpdated — 背包变动通知
/// </summary>
public static void BackpackUpdated(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<LoginBackpackData>>(msg).arguments[0];
// 刮刮卡面板打开期间挂起推送刷新:金币/钻石/道具统一在点击"领取"按钮时刷新
if (GuaGuaLeGPU.SuspendWsRefresh) return;
// 刷新用户数据(道具列表在 GetUser 返回中)
var userId = GameManager.Instance?.user?.id;
if (!string.IsNullOrEmpty(userId))
APIRequestList.GetUser(userId, a => GameManager.Instance.UpdataUserData(a));
}
/// <summary>
/// LoginHub NewEmail — 新邮件通知
/// </summary>
public static void NewEmail(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<LoginNewEmailData>>(msg).arguments[0];
UIManager.Instance.ShowBoardError($"收到新邮件:{data.title}");
}
/// <summary>
/// LoginHub KillAnnouncement — 全局击杀公告(特殊 Boss 击杀全服推送)
/// 客户端自行拼接公告文本(不再使用服务端 text):
/// 【风雷之渊】珠联璧合!恭喜:【小*鱼】捕获雷皇龙,获得47,640,000金币
/// </summary>
public static void KillAnnouncement(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<KillAnnouncementData>>(msg).arguments[0];
if (data == null) return;
// 恭喜词随机
string[] words = { "珠联璧合", "鸿运当头", "满载而归", "财源滚滚", "好运连连", "喜从天降", "手气爆棚", "天选之人" };
string word = words[UnityEngine.Random.Range(0, words.Length)];
// 外层默认色 ECA895,关键字段用 ECB85F 覆盖
string text = $"<color=#ECA895><color=#ECB85F>【{data.gameTypeName}】</color>{word}!恭喜:" +
$"<color=#ECB85F>【{MaskUserName(data.userName)}】</color>捕获{data.fishName}," +
$"获得<color=#ECB85F>{data.gold:N0}</color>金币</color>";
UIManager.Instance.ShowBoardGongGao(text);
}
/// <summary>玩家名脱敏:只显示首尾字,中间用*(如:小美人鱼 → 小*鱼)</summary>
private static string MaskUserName(string name)
{
if (string.IsNullOrEmpty(name)) return string.Empty;
if (name.Length == 1) return name;
return $"{name[0]}*{name[name.Length - 1]}";
}
/// <summary>
/// LoginHub LevelChanged — 后台修改玩家等级
/// 直接调用 GetUser 刷新用户数据,触发 OnUserDataChanged 同步 UI
/// </summary>
public static void LevelChanged(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<LevelChangedData>>(msg).arguments[0];
var userId = GameManager.Instance?.user?.id;
if (!string.IsNullOrEmpty(userId))
APIRequestList.GetUser(userId, a => GameManager.Instance.UpdataUserData(a));
}
// ---------------- LoginHub 断线房间 ----------------
/// <summary>当前是否有待处理的断线房间弹窗(防止重复弹)</summary>
private static bool _pendingRoomActive;
/// <summary>
/// LoginHub PendingRoomNotice — 登录后检测到有未完成的断线游戏
/// 弹窗提示用户选择"返回房间"(调 Reconnect)或"离开"(调 DismissPendingRoom)
/// 60 秒无操作自动离开
/// </summary>
public static void PendingRoomNotice(string msg)
{
if (_pendingRoomActive) return; // 防重复
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<PendingRoomData>>(msg).arguments[0];
if (data == null) return;
_pendingRoomActive = true;
var userId = GameManager.Instance?.user?.id;
if (string.IsNullOrEmpty(userId)) return;
// 创建 TiShi 弹窗(直接 Instantiate 拿引用,方便 60 秒后销毁)
var prefab = UIManager.Instance.TiShiParefab;
if (prefab == null)
{
Debug.LogWarning("[PendingRoomNotice] TiShiParefab 未赋值,自动离开");
WebSocketSendList.DismissPendingRoom(userId);
_pendingRoomActive = false;
return;
}
var obj = UnityEngine.Object.Instantiate(prefab, UIManager.Instance.uiRoot);
obj.transform.SetAsLastSibling();
var tishi = obj.GetComponent<TiShi>();
if (tishi == null)
{
UnityEngine.Object.Destroy(obj);
_pendingRoomActive = false;
return;
}
// 标记用户是否已操作
bool acted = false;
// 确认 = 返回房间
tishi.Init($"检测到您在 {data.gameType} {data.roomName} 有未完成的游戏,是否返回房间?", () =>
{
acted = true;
WebSocketSendList.Reconnect(userId);
});
// 关闭 = 离开(额外给 btn_Close 加监听;TiShi.Close 本身只销毁不触发 action)
if (tishi.btn_Close != null)
{
tishi.btn_Close.onClick.AddListener(() =>
{
if (acted) return;
acted = true;
WebSocketSendList.DismissPendingRoom(userId);
});
}
// 60 秒无操作自动离开
WebSocketManager.Instance.StartCoroutine(PendingRoomCountdown(
60f, () => acted, () =>
{
acted = true;
WebSocketSendList.DismissPendingRoom(userId);
if (obj != null) UnityEngine.Object.Destroy(obj);
_pendingRoomActive = false;
}));
}
/// <summary>断线房间弹窗倒计时协程(到时检查是否已操作,未操作则触发回调)</summary>
private static System.Collections.IEnumerator PendingRoomCountdown(
float seconds, System.Func<bool> hasActed, System.Action onTimeout)
{
yield return new UnityEngine.WaitForSeconds(seconds);
if (!hasActed())
onTimeout?.Invoke();
}
/// <summary>
/// LoginHub PendingRoomNotice 数据
/// </summary>
[Serializable]
public class PendingRoomData
{
public string roomId;
public string gameType;
public string roomName;
}
/// <summary>
/// LoginHub AccountKicked — 账号被顶通知
/// 该账号在其他设备登录时,旧连接收到此事件。
/// 前端应提示"账号已在其他地方登录"并清空房间 UI、断开游戏连接。
/// </summary>
public static void AccountKicked(string msg)
{
try
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<AccountKickedData>>(msg).arguments[0];
var userId = GameManager.Instance?.user?.id;
// 只处理当前登录用户被顶的情况
if (data != null && !string.IsNullOrEmpty(data.userId)
&& !string.IsNullOrEmpty(userId) && data.userId == userId)
{
UIManager.Instance.ShowTiShi("账号已在其他地方登录", () =>
{
GameManager.Instance.Logout();
return;
}, showClose: false);
}
}
catch (Exception e)
{
Debug.LogWarning("[AccountKicked] 解析失败: " + e.Message);
}
}
/// <summary>
/// LoginHub AccountKicked 数据
/// </summary>
[Serializable]
public class AccountKickedData
{
public string userId;
}
/// <summary>
/// LoginHub ChangeConfig — 后台活动配置或游戏配置保存发布后广播
/// changeType: 1=活动配置更新 2=游戏配置更新
/// 前端按 changeType 重新请求对应配置
/// </summary>
public static void ChangeConfig(string msg)
{
try
{
var changeType = JsonConvert.DeserializeObject<WebSocketResponseBase<int>>(msg).arguments[0];
switch (changeType)
{
case 1:
// 活动配置更新
APIRequestList.GetActivityConfig(res =>
{
if (res != null) GameManager.Instance.activityConfig = res;
});
break;
case 2:
// 游戏配置更新
APIManager.Response<GameConfig>(APIType.GameConfig_Get, res =>
{
if (res != null)
{
GameManager.Instance.gameConfig = res;
var v = GameManager.Instance.dic_fishTypeConfigs;
if (v != null)
{
v.Clear();
foreach (var item in res.fishTypeConfigs)
v[item.fishNumber] = item;
}
}
});
break;
}
}
catch (Exception e)
{
Debug.LogWarning("[ChangeConfig] 解析失败: " + e.Message);
}
}
/// <summary>
/// LoginHub KickToMainPage — 强制退出到主页面
/// 当前账号连接已失效(被顶号/用户不匹配),前端应立即清除所有游戏 UI 并导航到主页面/登录页
/// </summary>
public static void KickToMainPage(string msg)
{
try
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<KickToMainPageData>>(msg).arguments[0];
var userId = GameManager.Instance?.user?.id;
// 只处理当前登录用户被踢的情况
if (data != null && !string.IsNullOrEmpty(data.userId)
&& !string.IsNullOrEmpty(userId) && data.userId != userId)
return;
}
catch (Exception e)
{
Debug.LogWarning("[KickToMainPage] 解析失败: " + e.Message);
}
UIManager.Instance.ShowTiShi("连接已失效,请重新登录", () =>
{
GameManager.Instance.Logout();
}, showClose: false);
}
/// <summary>
/// LoginHub KickToMainPage 数据
/// </summary>
[Serializable]
public class KickToMainPageData
{
public string userId;
}
// ---------------- FishGameHub 补充 ----------------
/// <summary>
/// FishGameHub BotLeaveRoomNotice — 机器人退出房间
/// 仅触发对应座位显示等待,不影响本地玩家(自己退出走 LeaveRoomResult)
/// </summary>
public static void BotLeaveRoom(string botUserId)
{
if (string.IsNullOrEmpty(botUserId)) return;
// ⭐ 机器人爆奖中途退房:同样清掉其未完成的"爆奖中"BJEndUI(同真人退房)
BaoJiangNotSelfHandler.RemoveByUser(botUserId);
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.FLZY:
ReleaseBotSit(UIPanel.MainGamePanelFLZY, botUserId);
break;
case GameType.YYSC:
ReleaseBotSit(UIPanel.MainGamePanelYYSC, botUserId);
break;
case GameType.XJMH:
ReleaseBotSit(UIPanel.MainGamePanelXJMH, botUserId);
break;
case GameType.HHWK:
ReleaseBotSit(UIPanel.MainGamePanelHHWK, botUserId);
break;
default:
break;
}
}
/// <summary>释放机器人座位(显示等待界面,标记为 bot)</summary>
private static void ReleaseBotSit(UIPanel panelName, string botUserId)
{
var panel = UIManager.Instance.Get<GameMainPanel_FishBase>(panelName);
if (panel == null) return;
foreach (var sit in panel.list_Sits)
{
if (sit.user == botUserId)
{
sit.isBot = true;
sit.ShowWaiting();
break;
}
}
}
/// <summary>
/// FishGameHub HostDisconnect — 房主断线
/// 其他玩家可接受机器人控制权。本地房主引用若已断线则转移给本地首个非人机玩家。
/// </summary>
public static void HostDisconnect()
{
// 房主断线 → 触发房主转移(SitPosData 已封装好转移逻辑)
SitPosData.TransferRoomOwner();
}
/// <summary>
/// FishGameHub OnSeatChange — 座位变更
/// { type, userId, position, showWaiting? }
/// type 字段后端未明确定义,先按 showWaiting 决定显示状态
/// </summary>
public static void SeatChange(int seatType, string userId, int position, bool showWaiting)
{
if (string.IsNullOrEmpty(userId) || position < 0) return;
GameMainPanel_FishBase panel = null;
switch (GameManager.Instance.gameData.currGameType)
{
case GameType.FLZY:
panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelFLZY);
break;
case GameType.YYSC:
panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelYYSC);
break;
case GameType.XJMH:
panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelXJMH);
break;
case GameType.HHWK:
panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelHHWK);
break;
default:
break;
}
if (panel == null) return;
// ⭐ 座位镜像:服务端座位转显示座位再找场景座位
var targetSit = panel.list_Sits.Find(s => s.posID == SeatMirror.ToDisplay(position));
if (targetSit == null) return;
if (showWaiting)
{
// 显示等待界面(玩家离开该座位)
targetSit.ShowWaiting();
}
else
{
// 玩家进入该座位
targetSit.isBot = !string.IsNullOrEmpty(userId) && userId.StartsWith("bot_");
}
}
/// <summary>
/// FishRoomHub RoomChange — 房间变化通知
/// 房间列表界面收到时刷新显示。当前仅在房间列表 UI 开启时更新数据,
/// 具体刷新逻辑由 UIHome_BY_SelectRoomLevelPanel / 手动选座面板按需实现。
/// </summary>
public static void RoomChangeNotice(string msg)
{
try
{
var roomData = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomChangeResponse>>(msg).arguments[0];
if (roomData == null) return;
// ⭐ 修复:游戏中收到本房间的 RoomChange(players 是全量最新数据)→ 同步座位。
// room socket 进游戏后仍保持连接,玩家加入/离开时都会推 RoomChange;
// 若 game socket 侧没收到(或没处理)RoomJoinNotice/RoomLeaveNotice,
// 老客户端就识别不到新玩家。这里按 players 与 currRoomData.players 全量比对:
// 新加入的入座(JoinGame),离开的释放座位(ShowWaiting),幂等可重复。
SyncSeatsFromRoomChange(roomData);
// 通知手动选座面板刷新(若开启)
var homeMain = UIManager.Instance.Get<UIHomeMain>(UIPanel.HomeMainPanel);
if (homeMain == null) return;
var selectRoomPanel = homeMain.subUIManager.Get<UIHome_BY_SelectRoomLevelPanel>(UIPanel.SelectRoomLeavePenel);
if (selectRoomPanel != null && selectRoomPanel.isMySelectRoom)
{
selectRoomPanel.panel_UIHome_BY_MySelectRoomPenel?.OnRoomChange(roomData);
}
}
catch (Exception e)
{
Debug.LogWarning("[RoomChangeNotice] 解析失败: " + e.Message);
}
}
/// <summary>
/// 游戏中收到本房间 RoomChange 时同步座位:players 与 currRoomData.players 全量比对。
/// 新加入(列表有、快照无)→ JoinGame 入座;离开(快照有、列表无)→ ShowWaiting 释放座位。
/// 与 RoomJoinNotice/RoomLeaveNotice 处理幂等(快照里已有的玩家不会重复入座)。
/// </summary>
private static void SyncSeatsFromRoomChange(RoomChangeResponse roomData)
{
var currRoom = GameManager.Instance.gameData?.currRoomData;
// 只处理当前所在房间(房间列表里其他房间的 RoomChange 不关心)
if (currRoom == null || roomData.roomId != currRoom.roomId) return;
if (roomData.players == null) return;
if (currRoom.players == null) currRoom.players = new List<RoomPlayerInfo>();
var panel = UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelDefault)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelFLZY)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelYYSC)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelXJMH)
?? UIManager.Instance.Get<GameMainPanel_FishBase>(UIPanel.MainGamePanelHHWK);
// 1. 新加入:列表有、快照无 → 写入快照并入座
foreach (var p in roomData.players)
{
if (string.IsNullOrEmpty(p.userId)) continue;
if (currRoom.players.Exists(x => x.userId == p.userId)) continue;
currRoom.players.Add(p);
if (panel == null) continue; // 不在游戏中只补数据,进房流程自己会建座位
bool isBot = p.isBot || p.userId.StartsWith("bot_");
// ⭐ 座位镜像:服务端座位转显示座位再找场景座位(JoinGame 内部也做同样转换)
var targetSit = panel.list_Sits.Find(s => s.posID == SeatMirror.ToDisplay(p.position));
// 真人顶掉 bot 座位时先替换(与 RoomJoinNotice 分支同逻辑)
if (targetSit != null && targetSit.isBot && !isBot)
{
targetSit.isBot = false;
targetSit.ShowWaiting();
}
panel.JoinGame(p.position, p.gold, p.diamond, p.cannonMultiple, 0, p.userId, p.cannonId, p.wingsId, p.warGodId);
}
// 2. 离开:快照有、列表无 → 释放座位并移出快照
for (int i = currRoom.players.Count - 1; i >= 0; i--)
{
var p = currRoom.players[i];
if (string.IsNullOrEmpty(p.userId)) continue;
if (roomData.players.Exists(x => x.userId == p.userId)) continue;
currRoom.players.RemoveAt(i);
if (panel == null) continue;
foreach (var sit in panel.list_Sits)
{
if (sit.user == p.userId)
sit.ShowWaiting();
}
}
}
/// <summary>
/// LoginHub LevelChanged 数据(字段名对齐后端大写首字母)
/// </summary>
[Serializable]
public class LevelChangedData
{
public int NewLevel;
public long NewExp;
public long NewGold;
public int NewDiamond;
public string Source;
public string Remark;
}
// ---------------- Customer ----------------
/// <summary>
/// 发起会话回调(用户端)
/// isOk=false 时弹错误提示;isOk=true 时自动加入会话房间
/// </summary>
public static void Customer_OnConversationStarted(CustomerConversationStartedResult result)
{
if (!result.isOk)
{
UIManager.Instance.ShowBoardError(result.msg);
return;
}
// 加入会话房间,之后才能收发消息
//WebSocketSendList.Customer_JoinConversation(result.conversation.id);
GetActiveServicePanel()?.OnConversationStarted(result);
}
/// <summary>
/// 消息接收回调(用户端 & 客服端)
/// </summary>
public static void Customer_OnMessageReceived(CustomerMessageReceived msg)
{
GetActiveServicePanel()?.OnMessageReceived(msg);
Debug.Log($"[Customer] 收到消息 from {msg.senderName}({msg.senderType}): {msg.content}");
}
/// <summary>
/// 获取当前活动的客服面板(主界面或登录界面打开的)
/// </summary>
private static UIHome_BY_ServicePanel GetActiveServicePanel()
{
// 先尝试主界面
var home = UIManager.Instance.Get<UIHomeMain>(UIPanel.HomeMainPanel);
if (home != null && home.subUIManager != null)
{
var panel = home.subUIManager.Get<UIHome_BY_ServicePanel>(UIPanel.ServicePanel);
if (panel != null) return panel;
}
// 再尝试登录界面(游客模式)
var login = UIManager.Instance.Get<UILoginPanel>(UIPanel.LoginPanel);
if (login != null && login.uIManager != null)
{
return login.uIManager.Get<UIHome_BY_ServicePanel>(UIPanel.ServicePanel);
}
return null;
}
/// <summary>
/// 新会话通知(客服端)
/// </summary>
public static void Customer_OnNewConversation(CustomerNewConversationNotice notice)
{
// 客服端自动加入会话房间
WebSocketSendList.Customer_JoinConversation(notice.conversationId);
// TODO: 通知客服端 UI 显示新会话(替换为你的实际 Panel 调用)
// UIManager.Instance.Get<UICustomerServicePanel>(UIPanel.CustomerServicePanel)?.OnNewConversation(notice);
Debug.Log($"[Customer] 新会话来了,userId={notice.userId},userName={notice.userName}");
}
/// <summary>
/// LoginHub Broadcast 数据
/// </summary>
[Serializable]
public class LoginBroadcastData
{
public string userName;
public string avatarUrl;
public int level;
public int vipLevel;
public string message;
public long time;
}
/// <summary>
/// LoginHub KillAnnouncement 数据(全局击杀公告)
/// </summary>
[Serializable]
public class KillAnnouncementData
{
/// <summary>公告文本(服务端已拼接:恭喜玩家*****在风雷之渊 高级场捕获 人鱼公主*2000倍 150000000大奖)</summary>
public string text;
/// <summary>玩家名(已脱敏,全部替换为*)</summary>
public string userName;
/// <summary>游戏类型名(如:风雷之渊)</summary>
public string gameTypeName;
/// <summary>房间名(如:高级场)</summary>
public string roomName;
/// <summary>鱼名称(如:人鱼公主)</summary>
public string fishName;
/// <summary>炮台倍数</summary>
public int multiple;
/// <summary>获得金币</summary>
public long gold;
/// <summary>时间戳(Unix秒)</summary>
public long time;
}
/// <summary>
/// LoginHub AssetUpdated 数据
/// </summary>
[Serializable]
public class LoginAssetData
{
public int currencyType;
public int changeType;
public long amount;
public long newGold;
public int newDiamond;
public string source;
public string remark;
}
/// <summary>
/// LoginHub BackpackUpdated 数据
/// </summary>
[Serializable]
public class LoginBackpackData
{
public string action;
public string propNumber;
public int count;
public List<object> props;
}
/// <summary>
/// LoginHub NewEmail 数据
/// </summary>
[Serializable]
public class LoginNewEmailData
{
public string title;
public string emailId;
}
/// <summary>
/// FishGameHub Exception 数据({code, message})
/// code: 0=未知错误
/// 1001=房间已满 1002=金币不足 1003=房间不存在 1004=玩家不在房间
/// 1005=签名错误 1006=请求已过期 1007=鱼配置不存在 1008=子弹参数无效
/// 1009=金币参数超出范围 1011=用户不存在 1012=道具配置不存在
/// 1013=道具数量不足 1014=钻石不足 1015=炮倍参数无效
/// </summary>
[Serializable]
public class FishGameExceptionData
{
public int code;
public string message;
}
}
}
// end namespace