1488 lines
59 KiB
C#
1488 lines
59 KiB
C#
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Threading.Tasks;
|
||
using Logic;
|
||
using Logic.CrashSight;
|
||
using Logic.Multilingual;
|
||
using MemoryPack;
|
||
using RuntimeData;
|
||
using Steamworks;
|
||
using TH1_Core.Events;
|
||
using TH1_Core.Managers;
|
||
using TH1_Logic.Config;
|
||
using TH1_Logic.Core;
|
||
using TH1_Logic.Net;
|
||
using UnityEngine;
|
||
|
||
|
||
namespace TH1_Logic.Steam
|
||
{
|
||
// 网络状态信息结构
|
||
public struct SteamNetworkStatus
|
||
{
|
||
public bool IsSteamConnected; // Steam是否连接
|
||
public bool IsLoggedOn; // 是否登录
|
||
public bool IsP2PReady; // P2P是否就绪
|
||
public bool IsInLobby; // 是否在房间中
|
||
public int ConnectedPeersCount; // 已连接的P2P节点数量
|
||
public string ConnectionQuality; // 连接质量描述
|
||
public string DetailedStatus; // 详细状态描述
|
||
public bool HasActiveP2PConnections; // 是否有活跃的P2P连接
|
||
public bool SteamServerConnected; // Steam服务器连接状态
|
||
}
|
||
|
||
|
||
public class SteamLobbyManager : ILobby
|
||
{
|
||
// 登录状态
|
||
private bool _isLoggedIn = false;
|
||
public bool IsloggedIn => _isLoggedIn;
|
||
|
||
// steam 初始化状态
|
||
private bool _isSteamInitialized = false;
|
||
public bool IsSteamInitialized => _isSteamInitialized;
|
||
|
||
// lobby 初始化状态
|
||
private bool _isLobbyInitialized = false;
|
||
public bool IsLobbyInitialized => _isLobbyInitialized;
|
||
|
||
private SteamNetworkStatus _status;
|
||
public SteamNetworkStatus Status => _status;
|
||
|
||
// 个人信息
|
||
private string _selfName = "";
|
||
private CSteamID _selfID;
|
||
public string SelfName => _selfName;
|
||
public CSteamID SelfID => _selfID;
|
||
|
||
private int _maxLobbyMembers = 4;
|
||
private GameObject _guiObj;
|
||
|
||
public CSteamID CurrentLobby = CSteamID.Nil;
|
||
public CSteamID CachedOwner = CSteamID.Nil;
|
||
public LobbyState CurrentState { get; private set; } = LobbyState.None;
|
||
|
||
// 定时刷新
|
||
private float _steamSDKUpdateRecord;
|
||
private Dictionary<ulong, CSteamID> _onlineFriendsId;
|
||
private Dictionary<ulong, MemberInfo> _onlineFriendsInfo;
|
||
private Dictionary<ulong, MemberInfo> _memberInfos;
|
||
private Dictionary<ulong, CSteamID> _membersCache;
|
||
private float _onlineFriendsIdUpdateRecord;
|
||
private float _kickInfoUpdateRecord;
|
||
private float _refreshSteamStatus;
|
||
private bool _steamServerConnected = false;
|
||
private int _steamSessionFailCount;
|
||
private bool _isHandlingSessionLoss;
|
||
private int _suppressP2PSendFailureLobbyErrors;
|
||
private const float SteamSessionCheckInterval = 1f;
|
||
private const int SteamSessionFailThreshold = 2;
|
||
private string RoomName;
|
||
|
||
// 房间列表
|
||
private List<LobbyListInfo> _lobbyListInfos;
|
||
public List<LobbyListInfo> LobbyListInfos => _lobbyListInfos;
|
||
|
||
// 事件委托
|
||
public event System.Action<CSteamID> OnLobbyCreatedEvent; // 房间创建成功
|
||
public event System.Action<CSteamID> OnLobbyEnteredEvent; // 进入房间
|
||
public event System.Action<List<CSteamID>> OnLobbyLeftEvent; // 离开房间
|
||
public event System.Action<CSteamID, CSteamID> OnHostChangedEvent; // 房主变更 (oldHost, newHost)
|
||
public event System.Action<List<CSteamID>> OnMembersChangedEvent; // 成员变化
|
||
public event System.Action<string> OnLobbyErrorEvent; // 房间错误
|
||
public event System.Action<CSteamID> OnMemberJoinedEvent; // 成员加入
|
||
public event System.Action<CSteamID> OnMemberLeftEvent; // 成员离开
|
||
|
||
// LobbyCreated_t
|
||
// 谁会收到:只有调用 CreateLobby 的本机(创建者)。
|
||
// 何时触发:_kickInfoUpdateRecord.CreateLobby 异步完成后。
|
||
// 成功判定:data.m_eResult == k_EResultOK。失败则不会进入房间,也不会再收到 LobbyEnter_t。
|
||
// 用途:确认为房主,保存 LobbyID,设置 LobbyData / Rich Presence,然后等别人加入。
|
||
private Callback<LobbyCreated_t> _cbLobbyCreated;
|
||
|
||
// 谁会收到:被邀请者、或通过好友资料 / 邀请链接 / Overlay 点"加入"你 Lobby 的玩家客户端。
|
||
// 何时触发:玩家端点击接受邀请(或点击你在好友列表中的"加入游戏")后,Steam 向你的进程派发此回调。
|
||
// 典型处理:立即调用 SteamMatchmaking.JoinLobby(data.m_steamIDLobby)。不代表已经进房,只是"请求加入"。
|
||
private Callback<GameLobbyJoinRequested_t> _cbLobbyJoinRequested;
|
||
|
||
// 谁会收到:任意成功进入 Lobby 的客户端(包括创建者自己与每个加入者)。
|
||
// 何时触发:JoinLobby(或 CreateLobby 成功后的内部自动加入)完成并真正加入成员列表后。
|
||
// 作用:此时可读取成员列表 / LobbyData,更新 UI,开始建立 P2P。
|
||
// 注意:创建者会先收到 LobbyCreated_t,随后收到自己的 LobbyEnter_t。被邀请者只会收到 GameLobbyJoinRequested_t →(调用 JoinLobby)→ LobbyEnter_t。
|
||
private Callback<LobbyEnter_t> _cbLobbyEnter;
|
||
|
||
// 谁会收到:当前已在该 Lobby 中的所有成员。
|
||
// 何时触发:成员进入、离开、断线、被踢、被封禁等成员列表变化时。可能多次。
|
||
// 数据意义:data.m_ulSteamIDLobby = 目标 Lobby;data.m_ulSteamIDUserChanged = 发生变化的成员;data.m_ulSteamIDMakingChange = 触发者(踢人时是房主);data.m_rgfChatMemberStateChange 标志位指示加入/离开/踢/封禁等。
|
||
// 用途:刷新成员 UI,迁移 Host(若房主离开), 清理对应的 P2P 连接。
|
||
private Callback<LobbyChatUpdate_t> _cbLobbyChatUpdate;
|
||
private Callback<SteamServersConnected_t> _cbSteamServersConnected;
|
||
private Callback<SteamServersDisconnected_t> _cbSteamServersDisconnected;
|
||
private Callback<SteamServerConnectFailure_t> _cbSteamServerConnectFailure;
|
||
private Callback<SteamShutdown_t> _cbSteamShutdown;
|
||
|
||
// 搜索公开房间
|
||
private Callback<LobbyMatchList_t> _cbLobbyMatchList;
|
||
|
||
|
||
public SteamLobbyManager()
|
||
{
|
||
_steamSDKUpdateRecord = 2;
|
||
_onlineFriendsIdUpdateRecord = 2;
|
||
_kickInfoUpdateRecord = 2;
|
||
_refreshSteamStatus = 0;
|
||
_steamSessionFailCount = 0;
|
||
_isHandlingSessionLoss = false;
|
||
_onlineFriendsId = new Dictionary<ulong, CSteamID>();
|
||
_onlineFriendsInfo = new Dictionary<ulong, MemberInfo>();
|
||
_memberInfos = new Dictionary<ulong, MemberInfo>();
|
||
_membersCache = new Dictionary<ulong, CSteamID>();
|
||
_lobbyListInfos = new List<LobbyListInfo>();
|
||
|
||
_status = new SteamNetworkStatus();
|
||
}
|
||
|
||
// 初始化
|
||
public void Init()
|
||
{
|
||
_steamSDKUpdateRecord += Time.deltaTime;
|
||
if (_steamSDKUpdateRecord < 2) return;
|
||
_steamSDKUpdateRecord = 0;
|
||
RefreshSteamInit();
|
||
RefreshSteamStatus();
|
||
//RefreshSteamGUI();
|
||
RefreshLoginStatus();
|
||
RefreshLobbyStatus();
|
||
}
|
||
|
||
// 刷新 Steam
|
||
private void RefreshSteamInit()
|
||
{
|
||
if (_isSteamInitialized) return;
|
||
|
||
// 检查Steam是否运行
|
||
if (!SteamAPI.IsSteamRunning())
|
||
{
|
||
// LogSystem.LogError("Steam客户端未运行!请先启动Steam。");
|
||
return;
|
||
}
|
||
|
||
// 初始化Steam API
|
||
LogSystem.LogInfo("开始初始化Steam...");
|
||
var initResult = SteamAPI.InitEx(out string steamErrMsg);
|
||
_isSteamInitialized = initResult == ESteamAPIInitResult.k_ESteamAPIInitResult_OK;
|
||
|
||
if (!_isSteamInitialized)
|
||
{
|
||
LogSystem.LogError($"Steam API初始化失败!result={initResult}, msg={steamErrMsg}");
|
||
switch (initResult)
|
||
{
|
||
case ESteamAPIInitResult.k_ESteamAPIInitResult_NoSteamClient:
|
||
LogSystem.LogError("→ Steam客户端未运行或未登录,请先打开Steam并登录账号。");
|
||
break;
|
||
case ESteamAPIInitResult.k_ESteamAPIInitResult_VersionMismatch:
|
||
LogSystem.LogError("→ Steam客户端版本过旧,请在Steam中检查更新。");
|
||
break;
|
||
case ESteamAPIInitResult.k_ESteamAPIInitResult_FailedGeneric:
|
||
LogSystem.LogError("→ 通用失败。常见原因:当前Steam账号未拥有该AppID(需在Steamworks后台授予权限);或工作目录下steam_appid.txt位置错误;或上一次进程未退干净。");
|
||
break;
|
||
}
|
||
return;
|
||
}
|
||
LogSystem.LogInfo("Steam API初始化成功!");
|
||
// 显示启动信息
|
||
DisplayLaunchInfo();
|
||
CheckSteamAppIdFile();
|
||
}
|
||
|
||
// 刷新 Steam 状态
|
||
private void RefreshSteamStatus()
|
||
{
|
||
if (!_isSteamInitialized) return;
|
||
|
||
// 基础Steam连接状态
|
||
_status.IsSteamConnected = SteamAPI.IsSteamRunning();
|
||
_status.IsLoggedOn = SteamUser.BLoggedOn();
|
||
_status.IsInLobby = IsInLobby();
|
||
_status.IsP2PReady = SimpleP2P.Instance?.IsInitialized ?? false;
|
||
_status.SteamServerConnected = IsSteamSessionLikelyAlive();
|
||
_steamServerConnected = _status.SteamServerConnected;
|
||
}
|
||
|
||
// 刷新用户登录状态
|
||
private void RefreshLoginStatus()
|
||
{
|
||
if (!_isSteamInitialized || _isLoggedIn) return;
|
||
try
|
||
{
|
||
_isLoggedIn = SteamUser.BLoggedOn();
|
||
if (_isLoggedIn)
|
||
{
|
||
_selfID = SteamUser.GetSteamID();
|
||
_selfName = SteamFriends.GetPersonaName();
|
||
LogSystem.LogInfo($"Steam用户已登录: {_selfName} ({_selfID})");
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogWarning("Steam用户未登录");
|
||
}
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
LogSystem.LogError($"检查用户登录状态异常: {e.Message}");
|
||
}
|
||
}
|
||
|
||
// 刷新 lobby 相关
|
||
private void RefreshLobbyStatus()
|
||
{
|
||
if (!_isSteamInitialized || !_isLoggedIn) return;
|
||
if (_isLobbyInitialized) return;
|
||
_cbLobbyCreated = Callback<LobbyCreated_t>.Create(OnLobbyCreatedCallback);
|
||
_cbLobbyJoinRequested = Callback<GameLobbyJoinRequested_t>.Create(OnLobbyJoinRequestedCallback);
|
||
_cbLobbyEnter = Callback<LobbyEnter_t>.Create(OnLobbyEnterCallback);
|
||
_cbLobbyChatUpdate = Callback<LobbyChatUpdate_t>.Create(OnLobbyChatUpdateCallback);
|
||
_cbSteamServersConnected = Callback<SteamServersConnected_t>.Create(OnSteamServersConnectedCallback);
|
||
_cbSteamServersDisconnected = Callback<SteamServersDisconnected_t>.Create(OnSteamServersDisconnectedCallback);
|
||
_cbSteamServerConnectFailure = Callback<SteamServerConnectFailure_t>.Create(OnSteamServerConnectFailureCallback);
|
||
_cbSteamShutdown = Callback<SteamShutdown_t>.Create(OnSteamShutdownCallback);
|
||
|
||
// 初始化P2P
|
||
SimpleP2P.Instance.Initialize();
|
||
|
||
// 订阅P2P事件
|
||
SimpleP2P.Instance.OnPeerConnectedEvent += OnP2PPeerConnected;
|
||
SimpleP2P.Instance.OnPeerDisconnectedEvent += OnP2PPeerDisconnected;
|
||
SimpleP2P.Instance.OnConnectionErrorEvent += OnP2PConnectionError;
|
||
SimpleP2P.Instance.OnMessageSendFailedEvent += OnP2PMessageSendFailed;
|
||
|
||
LogSystem.LogInfo("SteamLobbyManager initialized");
|
||
_isLobbyInitialized = true;
|
||
}
|
||
|
||
// 定时更新在线好友
|
||
private void RefreshOnlineFriends()
|
||
{
|
||
_onlineFriendsIdUpdateRecord += Time.deltaTime;
|
||
if (_onlineFriendsIdUpdateRecord > 2)
|
||
{
|
||
_onlineFriendsIdUpdateRecord = 0;
|
||
var friends = GetOnlineFriends();
|
||
|
||
// 添加房间内的非好友成员
|
||
if (CurrentLobby.IsValid())
|
||
{
|
||
int memberCount = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
|
||
for (int i = 0; i < memberCount; i++)
|
||
{
|
||
var memberId = SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
|
||
// 跳过自己
|
||
if (memberId == SteamUser.GetSteamID()) continue;
|
||
// 检查是否已在好友列表中
|
||
if (friends.All(f => f.id != memberId))
|
||
{
|
||
string memberName = SteamFriends.GetFriendPersonaName(memberId);
|
||
friends.Add((memberId, memberName));
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var kv in friends)
|
||
{
|
||
if (!_membersCache.ContainsKey(kv.id.m_SteamID)) _membersCache[kv.id.m_SteamID] = kv.id;
|
||
if (!_onlineFriendsId.ContainsKey(kv.id.m_SteamID))
|
||
{
|
||
_onlineFriendsId[kv.id.m_SteamID] = kv.id;
|
||
}
|
||
|
||
if (!_onlineFriendsInfo.ContainsKey(kv.id.m_SteamID))
|
||
{
|
||
_onlineFriendsInfo[kv.id.m_SteamID] = new MemberInfo();
|
||
_onlineFriendsInfo[kv.id.m_SteamID].Id = kv.id.m_SteamID;
|
||
_onlineFriendsInfo[kv.id.m_SteamID].Name = kv.name;
|
||
_onlineFriendsInfo[kv.id.m_SteamID].Texture = GetMemberAvatar(kv.id.m_SteamID);
|
||
}
|
||
}
|
||
|
||
foreach (var kv in _onlineFriendsInfo)
|
||
{
|
||
if (kv.Value.Texture == null) kv.Value.Texture = GetMemberAvatar(kv.Key);
|
||
}
|
||
|
||
foreach (var kv in _memberInfos)
|
||
{
|
||
if (kv.Value.Texture == null) kv.Value.Texture = GetMemberAvatar(kv.Key);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 定时刷新踢人信息
|
||
private void RefreshKickInfo()
|
||
{
|
||
_kickInfoUpdateRecord += Time.deltaTime;
|
||
if (_kickInfoUpdateRecord > 2)
|
||
{
|
||
_kickInfoUpdateRecord = 0;
|
||
CheckIfKicked();
|
||
}
|
||
}
|
||
|
||
// 定时更新
|
||
public void Update()
|
||
{
|
||
Init();
|
||
if (!_isSteamInitialized || !_isLoggedIn || !_isLobbyInitialized) return;
|
||
|
||
// 更新Steam回调
|
||
SteamAPI.RunCallbacks();
|
||
|
||
SimpleP2P.Instance.Update();
|
||
SimpleP2P.Instance.PollMessages();
|
||
|
||
RefreshOnlineFriends();
|
||
RefreshKickInfo();
|
||
CheckSteamSessionHealth();
|
||
}
|
||
|
||
private void CheckSteamSessionHealth()
|
||
{
|
||
if (!IsInLobby() || _isHandlingSessionLoss)
|
||
{
|
||
_steamSessionFailCount = 0;
|
||
return;
|
||
}
|
||
|
||
_refreshSteamStatus += Time.deltaTime;
|
||
if (_refreshSteamStatus < SteamSessionCheckInterval) return;
|
||
_refreshSteamStatus = 0f;
|
||
|
||
var alive = IsSteamSessionLikelyAlive();
|
||
if (alive)
|
||
{
|
||
_steamServerConnected = true;
|
||
_steamSessionFailCount = 0;
|
||
return;
|
||
}
|
||
|
||
_steamServerConnected = false;
|
||
_steamSessionFailCount++;
|
||
if (_steamSessionFailCount < SteamSessionFailThreshold) return;
|
||
HandleLocalSteamSessionLost("Steam session health check failed");
|
||
}
|
||
|
||
private void HandleLocalSteamSessionLost(string reason)
|
||
{
|
||
var inMultiGame = Main.MapData?.Net != null && Main.MapData.Net.Mode == NetMode.Multi;
|
||
if (!IsInLobby() && !inMultiGame) return;
|
||
if (_isHandlingSessionLoss) return;
|
||
_isHandlingSessionLoss = true;
|
||
|
||
LogSystem.LogWarning($"Local Steam session lost: {reason}");
|
||
ForceQuitCurrentMultiplayerSession(reason);
|
||
}
|
||
|
||
// 从房间异常退出时
|
||
private void ForceQuitCurrentMultiplayerSession(string reason)
|
||
{
|
||
LogSystem.LogWarning($"Force leaving multiplayer session. reason: {reason}");
|
||
if (Main.Instance?.GameLogic != null &&
|
||
Main.MapData?.Net != null &&
|
||
Main.MapData.Net.Mode == NetMode.Multi &&
|
||
Main.Instance.GameLogic.GetCurState() != GameState.Menu)
|
||
{
|
||
EventManager.Publish(new ExecuteUIBottomBottomBarQuit());
|
||
}
|
||
}
|
||
|
||
// 简单联机健康判定:需 Steam 运行 + 已登录 + 中继网络就绪
|
||
public bool IsSteamSessionLikelyAlive()
|
||
{
|
||
if (!_isSteamInitialized) return false;
|
||
if (!SteamAPI.IsSteamRunning()) return false;
|
||
if (!SteamUser.BLoggedOn()) return false;
|
||
|
||
var avail = SteamNetworkingUtils.GetRelayNetworkStatus(out var details);
|
||
bool relayOk = avail == ESteamNetworkingAvailability.k_ESteamNetworkingAvailability_Current
|
||
|| details.m_eAvail == ESteamNetworkingAvailability.k_ESteamNetworkingAvailability_Current;
|
||
return relayOk;
|
||
}
|
||
|
||
// 检查 steam_appid.txt 文件
|
||
private void CheckSteamAppIdFile()
|
||
{
|
||
string steamAppIdPath = Path.Combine(Directory.GetCurrentDirectory(), "steam_appid.txt");
|
||
|
||
if (File.Exists(steamAppIdPath))
|
||
{
|
||
try
|
||
{
|
||
string content = File.ReadAllText(steamAppIdPath).Trim();
|
||
LogSystem.LogInfo($"发现steam_appid.txt文件,App ID: {content}");
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
LogSystem.LogWarning($"读取steam_appid.txt失败: {e.Message}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogInfo("未发现steam_appid.txt文件 - 这在Steam平台发布时是正常的");
|
||
}
|
||
}
|
||
|
||
// 启动信息
|
||
private void DisplayLaunchInfo()
|
||
{
|
||
if (!_isSteamInitialized) return;
|
||
|
||
// 获取当前App ID
|
||
var currentAppId = SteamUtils.GetAppID();
|
||
LogSystem.LogInfo($"当前Steam App ID: {currentAppId}");
|
||
|
||
// 检查启动方式
|
||
bool launchedViaSteam = SteamApps.BIsSubscribedApp(currentAppId);
|
||
LogSystem.LogInfo($"通过Steam启动: {(launchedViaSteam ? "是" : "否")}");
|
||
|
||
// 显示Steam环境信息
|
||
LogSystem.LogInfo($"Steam语言: {SteamApps.GetCurrentGameLanguage()}");
|
||
LogSystem.LogInfo($"Steam服务器连接: {(SteamUser.BLoggedOn() ? "已连接" : "未连接")}");
|
||
|
||
// 检查DLC和订阅状态
|
||
if (currentAppId.m_AppId == 480) // Spacewar测试应用
|
||
{
|
||
LogSystem.LogInfo("当前使用Spacewar测试应用 - 适用于开发测试");
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogInfo($"当前使用正式应用ID: {currentAppId.m_AppId}");
|
||
}
|
||
}
|
||
|
||
// 建房
|
||
public void CreateLobby(int maxMembers = 4, bool isPublic = true)
|
||
{
|
||
if (CurrentState != LobbyState.None)
|
||
{
|
||
LogSystem.LogInfo($"Cannot create lobby in state: {CurrentState}");
|
||
return;
|
||
}
|
||
|
||
CurrentState = LobbyState.Creating;
|
||
LogSystem.LogInfo($"Creating public lobby with max members: {maxMembers}");
|
||
SteamMatchmaking.CreateLobby(isPublic?ELobbyType.k_ELobbyTypePublic:ELobbyType.k_ELobbyTypeFriendsOnly, maxMembers);
|
||
}
|
||
|
||
// 加入房间
|
||
public void JoinLobby(CSteamID lobbyId)
|
||
{
|
||
if (CurrentState != LobbyState.None)
|
||
{
|
||
//LogSystem.LogInfo($"Cannot join lobby in state: {CurrentState}");
|
||
//return;
|
||
}
|
||
|
||
// TODO 这里会涉及到房间子类对于UI的调用,对于房间的多态是不合理的,暂不处理,糊屎
|
||
var version = SteamMatchmaking.GetLobbyData(lobbyId, "Version");
|
||
if (!string.IsNullOrEmpty(version) && ConfigManager.Instance.VersionCfg.CurVersionInfo.Version != version)
|
||
{
|
||
LogSystem.LogInfo($"版本不一致 !!!");
|
||
// UI 弹框在这里
|
||
return;
|
||
}
|
||
|
||
LogSystem.LogInfo($"Joining lobby: {lobbyId}");
|
||
CurrentState = LobbyState.Joining;
|
||
SteamMatchmaking.JoinLobby(lobbyId);
|
||
}
|
||
|
||
// 离开房间
|
||
public void LeaveLobby()
|
||
{
|
||
if (!CurrentLobby.IsValid() || CurrentState == LobbyState.None) return;
|
||
|
||
CurrentState = LobbyState.Leaving;
|
||
LogSystem.LogInfo("Leaving lobby");
|
||
|
||
// 断开所有P2P连接
|
||
SimpleP2P.Instance.DisconnectAll();
|
||
SteamMatchmaking.LeaveLobby(CurrentLobby);
|
||
GameNetSender.Instance.ClearLobbyDataSyncState();
|
||
ResetLobbyState();
|
||
OnLobbyLeftEvent?.Invoke(null);
|
||
}
|
||
|
||
// 解散房间(仅房主可用)
|
||
public void DisbandLobby()
|
||
{
|
||
if (!IsLobbyOwner())
|
||
{
|
||
LogSystem.LogInfo("Only lobby owner can disband the lobby");
|
||
return;
|
||
}
|
||
|
||
LogSystem.LogInfo("Disbanding lobby");
|
||
// 设置房间数据标记解散
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "disbanded", "true");
|
||
// 踢出所有其他成员
|
||
foreach (var member in EnumerateMembers())
|
||
{
|
||
if (member != SteamUser.GetSteamID())
|
||
{
|
||
// Steam没有直接踢人API,通过设置数据让客户端自动离开
|
||
SteamMatchmaking.SetLobbyMemberData(CurrentLobby, "kicked", "true");
|
||
}
|
||
}
|
||
|
||
// 房主最后离开
|
||
LeaveLobby();
|
||
}
|
||
|
||
// 踢出指定成员(仅房主可用)
|
||
public void KickMember(ulong memberId)
|
||
{
|
||
if (!IsLobbyOwner())
|
||
{
|
||
LogSystem.LogError("Only lobby owner can kick members");
|
||
return;
|
||
}
|
||
|
||
if (memberId == SteamUser.GetSteamID().m_SteamID)
|
||
{
|
||
LogSystem.LogError("Cannot kick yourself");
|
||
return;
|
||
}
|
||
|
||
LogSystem.LogInfo($"Kicking member: {memberId}");
|
||
|
||
// 房主写 LobbyData(只有房主有权限设 LobbyData),所有客户端都能 GetLobbyData 读到
|
||
// 之前用的是 SetLobbyMemberData,那个 API 只能设自己的成员数据,写出去对方根本读不到 —— 协议不匹配
|
||
// 被踢方在 RefreshKickInfo → CheckIfKicked 里读 kick_{selfId},发现是 "true" 就自动 LeaveLobby
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, $"kick_{memberId}", "true");
|
||
}
|
||
|
||
// 通过房间ID直接加入(无需好友关系)
|
||
public void JoinLobbyById(ulong lobbyId)
|
||
{
|
||
var lobbyCSteamId = new CSteamID(lobbyId);
|
||
JoinLobby(lobbyCSteamId);
|
||
}
|
||
|
||
// 获取当前房间ID用于分享
|
||
public ulong GetShareableLobbyId()
|
||
{
|
||
return CurrentLobby.IsValid() ? CurrentLobby.m_SteamID : 0;
|
||
}
|
||
|
||
// 生成房间码(简化的Base36编码)
|
||
public string GenerateRoomCode()
|
||
{
|
||
if (!CurrentLobby.IsValid()) return "";
|
||
|
||
ulong lobbyId = CurrentLobby.m_SteamID;
|
||
return Base36Encode(lobbyId);
|
||
}
|
||
|
||
// 通过房间码加入
|
||
public void JoinByRoomCode(string code)
|
||
{
|
||
ulong lobbyId = Base36Decode(code);
|
||
if (lobbyId == 0)
|
||
{
|
||
LogSystem.LogError("Invalid room code");
|
||
return;
|
||
}
|
||
JoinLobbyById(lobbyId);
|
||
}
|
||
|
||
private string Base36Encode(ulong value)
|
||
{
|
||
const string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||
var result = new System.Text.StringBuilder();
|
||
while (value > 0)
|
||
{
|
||
result.Insert(0, chars[(int)(value % 36)]);
|
||
value /= 36;
|
||
}
|
||
return result.ToString();
|
||
}
|
||
|
||
private ulong Base36Decode(string code)
|
||
{
|
||
const string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||
ulong result = 0;
|
||
foreach (char c in code.ToUpper())
|
||
{
|
||
int index = chars.IndexOf(c);
|
||
if (index < 0) return 0;
|
||
result = result * 36 + (ulong)index;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// 邀请非好友(需要对方Steam ID)
|
||
public string GenerateUserCode()
|
||
{
|
||
if (!_isSteamInitialized || !_isLoggedIn) return null;
|
||
return Base36Encode(SteamUser.GetSteamID().m_SteamID);
|
||
}
|
||
|
||
public void InviteByRawSteamId(string code)
|
||
{
|
||
if (!CurrentLobby.IsValid())
|
||
{
|
||
LogSystem.LogError("Not in a lobby");
|
||
return;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(code))
|
||
{
|
||
LogSystem.LogError("Invalid user code");
|
||
return;
|
||
}
|
||
|
||
ulong steamId = Base36Decode(code);
|
||
if (steamId == 0)
|
||
{
|
||
LogSystem.LogError("Invalid user code format");
|
||
return;
|
||
}
|
||
|
||
var targetId = new CSteamID(steamId);
|
||
if (!targetId.IsValid())
|
||
{
|
||
LogSystem.LogError("Invalid Steam ID");
|
||
return;
|
||
}
|
||
|
||
LogSystem.LogInfo($"Inviting user by code: {code} (Steam ID: {steamId})");
|
||
SteamMatchmaking.InviteUserToLobby(CurrentLobby, targetId);
|
||
}
|
||
|
||
// 切换房间类型(仅房主可用)
|
||
public bool SetLobbyType(ELobbyType lobbyType)
|
||
{
|
||
if (!IsLobbyOwner())
|
||
{
|
||
LogSystem.LogError("Only lobby owner can change lobby type");
|
||
return false;
|
||
}
|
||
|
||
if (!CurrentLobby.IsValid())
|
||
{
|
||
LogSystem.LogError("Not in a lobby");
|
||
return false;
|
||
}
|
||
|
||
bool success = SteamMatchmaking.SetLobbyType(CurrentLobby, lobbyType);
|
||
if (success)
|
||
{
|
||
LogSystem.LogInfo($"Lobby type changed to: {lobbyType}");
|
||
}
|
||
else
|
||
{
|
||
LogSystem.LogError($"Failed to change lobby type to: {lobbyType}");
|
||
}
|
||
return success;
|
||
}
|
||
|
||
// 便捷方法:切换到公开房间
|
||
public bool SetLobbyPublic()
|
||
{
|
||
return SetLobbyType(ELobbyType.k_ELobbyTypePublic);
|
||
}
|
||
|
||
// 便捷方法:切换到仅好友
|
||
public bool SetLobbyFriendsOnly()
|
||
{
|
||
return SetLobbyType(ELobbyType.k_ELobbyTypeFriendsOnly);
|
||
}
|
||
|
||
// 发送游戏内邀请 不走 Steam 邀请框
|
||
public bool SendGameInvite(ulong targetSteamId)
|
||
{
|
||
var lobbyInfo = new LobbyListInfo
|
||
{
|
||
LobbyId = CurrentLobby.m_SteamID,
|
||
OwnerId = _selfID.m_SteamID,
|
||
|
||
OwnerName = SteamMatchmaking.GetLobbyData(CurrentLobby, "Owner"),
|
||
RoomName = SteamMatchmaking.GetLobbyData(CurrentLobby, "RoomName"),
|
||
Version = SteamMatchmaking.GetLobbyData(CurrentLobby, "Version"),
|
||
CurrentPlayers = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby),
|
||
MaxPlayers = SteamMatchmaking.GetLobbyMemberLimit(CurrentLobby),
|
||
};
|
||
var data = new InviteMessage();
|
||
data.LobbyInfo = lobbyInfo;
|
||
byte[] bytes = MemoryPackSerializer.Serialize<BaseMessage>(data);
|
||
// 优先从缓存获取,否则直接构造 CSteamID
|
||
var targetId = _onlineFriendsId.TryGetValue(targetSteamId, out var cachedId) ? cachedId : new CSteamID(targetSteamId);
|
||
|
||
if (!targetId.IsValid())
|
||
{
|
||
LogSystem.LogError($"Invalid target Steam ID: {targetSteamId}");
|
||
return false;
|
||
}
|
||
|
||
bool isSucceed = SimpleP2P.Instance.SendToWithOutConnect(targetId, bytes);
|
||
if (!isSucceed)
|
||
{
|
||
LogSystem.LogError($"Failed to send game invite to: {targetSteamId}");
|
||
return false;
|
||
}
|
||
LogSystem.LogInfo($"Game invite sent to: {targetSteamId}");
|
||
return true;
|
||
}
|
||
|
||
// 搜索房间
|
||
public void SearchPublicLobbies(ELobbyDistanceFilter filter = ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide,
|
||
int count = 100, string roomName = "", string romeCode="", bool onlyMenu = true)
|
||
{
|
||
_cbLobbyMatchList = Callback<LobbyMatchList_t>.Create(OnLobbyMatchListCallback);
|
||
|
||
// 添加筛选条件
|
||
SteamMatchmaking.AddRequestLobbyListDistanceFilter(filter);
|
||
// 限制返回数量
|
||
SteamMatchmaking.AddRequestLobbyListResultCountFilter(count);
|
||
// 标记游戏名,确保只搜索到本游戏的房间,其实只在测试状态下有用
|
||
SteamMatchmaking.AddRequestLobbyListStringFilter("Game", "TOHOTOPIA", ELobbyComparison.k_ELobbyComparisonEqual);
|
||
// 只筛选未进游戏的房间(菜单状态)
|
||
if (onlyMenu)
|
||
SteamMatchmaking.AddRequestLobbyListStringFilter("GameState", "0", ELobbyComparison.k_ELobbyComparisonEqual);
|
||
// 按名称筛选
|
||
if (!string.IsNullOrEmpty(roomName))
|
||
SteamMatchmaking.AddRequestLobbyListStringFilter("RoomName", roomName, ELobbyComparison.k_ELobbyComparisonEqual);
|
||
// 搜索制定房间码
|
||
if (!string.IsNullOrEmpty(romeCode))
|
||
SteamMatchmaking.AddRequestLobbyListStringFilter("RoomCode", romeCode, ELobbyComparison.k_ELobbyComparisonEqual);
|
||
// 数值筛选:只显示人数未满的房间
|
||
// SteamMatchmaking.AddRequestLobbyListNumericalFilter("open_slots", 1, ELobbyComparison.k_ELobbyComparisonGreaterThan);
|
||
SteamMatchmaking.RequestLobbyList();
|
||
}
|
||
|
||
// 搜索结果回调
|
||
private void OnLobbyMatchListCallback(LobbyMatchList_t data)
|
||
{
|
||
_lobbyListInfos.Clear();
|
||
|
||
for (int i = 0; i < data.m_nLobbiesMatching; i++)
|
||
{
|
||
var lobbyId = SteamMatchmaking.GetLobbyByIndex(i);
|
||
var ownerId = SteamMatchmaking.GetLobbyOwner(lobbyId);
|
||
|
||
_lobbyListInfos.Add(new LobbyListInfo
|
||
{
|
||
LobbyId = lobbyId.m_SteamID,
|
||
OwnerId = ownerId.m_SteamID,
|
||
OwnerName = SteamMatchmaking.GetLobbyData(lobbyId, "Owner"),
|
||
RoomName = SteamMatchmaking.GetLobbyData(lobbyId, "RoomName"),
|
||
Version = SteamMatchmaking.GetLobbyData(lobbyId, "Version"),
|
||
CurrentPlayers = SteamMatchmaking.GetNumLobbyMembers(lobbyId),
|
||
MaxPlayers = SteamMatchmaking.GetLobbyMemberLimit(lobbyId),
|
||
GameState = int.Parse(SteamMatchmaking.GetLobbyData(lobbyId, "GameState")),
|
||
});
|
||
}
|
||
|
||
// 触发UI刷新事件
|
||
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
|
||
}
|
||
|
||
// 刷新房间信息
|
||
public void RefreshLobbyListInfo()
|
||
{
|
||
foreach (var lobbyInfo in _lobbyListInfos)
|
||
{
|
||
var cSteamId = new CSteamID(lobbyInfo.LobbyId);
|
||
lobbyInfo.OwnerName = SteamMatchmaking.GetLobbyData(cSteamId, "Owner");
|
||
lobbyInfo.RoomName = SteamMatchmaking.GetLobbyData(cSteamId, "RoomName");
|
||
lobbyInfo.Version = SteamMatchmaking.GetLobbyData(cSteamId, "Version");
|
||
lobbyInfo.CurrentPlayers = SteamMatchmaking.GetNumLobbyMembers(cSteamId);
|
||
lobbyInfo.MaxPlayers = SteamMatchmaking.GetLobbyMemberLimit(cSteamId);
|
||
lobbyInfo.GameState = int.Parse(SteamMatchmaking.GetLobbyData(cSteamId, "GameState"));
|
||
}
|
||
}
|
||
|
||
// 过滤房间名称,只保留中文、英文字母、数字和常用符号
|
||
public static string FilterRoomName(string input, int maxLength = 20)
|
||
{
|
||
if (string.IsNullOrEmpty(input)) return "Default";
|
||
|
||
var result = new System.Text.StringBuilder();
|
||
|
||
foreach (char c in input)
|
||
{
|
||
// 中文字符范围:\u4e00-\u9fff
|
||
bool isChinese = c >= '\u4e00' && c <= '\u9fff';
|
||
// 英文字母
|
||
bool isLetter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||
// 数字
|
||
bool isDigit = c >= '0' && c <= '9';
|
||
// 常用符号(可根据需要调整)
|
||
bool isAllowedSymbol = c == '_' || c == '-' || c == '.';
|
||
|
||
if (isChinese || isLetter || isDigit || isAllowedSymbol)
|
||
{
|
||
result.Append(c);
|
||
}
|
||
|
||
// 达到最大长度时停止
|
||
if (result.Length >= maxLength) break;
|
||
}
|
||
if (string.IsNullOrEmpty(result.ToString())) return "Default";
|
||
return result.ToString();
|
||
}
|
||
|
||
public bool IsInitialized()
|
||
{
|
||
return _isSteamInitialized && _isLobbyInitialized && _isLoggedIn && SimpleP2P.Instance.IsInitialized;
|
||
}
|
||
|
||
public void InviteFriend(ulong memberId)
|
||
{
|
||
if (!CurrentLobby.IsValid())
|
||
{
|
||
LogSystem.LogError("Not in a lobby");
|
||
return;
|
||
}
|
||
|
||
var isSucceed = SendGameInvite(memberId);
|
||
if (isSucceed) return;
|
||
|
||
if (!_onlineFriendsId.ContainsKey(memberId)) return;
|
||
LogSystem.LogInfo($"Inviting friend: {memberId}");
|
||
SteamMatchmaking.InviteUserToLobby(CurrentLobby, _onlineFriendsId[memberId]);
|
||
}
|
||
|
||
// 打开好友邀请对话框
|
||
public void OpenInviteOverlay()
|
||
{
|
||
if (!CurrentLobby.IsValid())
|
||
{
|
||
LogSystem.LogError("Not in a lobby");
|
||
return;
|
||
}
|
||
|
||
SteamFriends.ActivateGameOverlayInviteDialog(CurrentLobby);
|
||
}
|
||
|
||
// 获取在线好友
|
||
public List<(CSteamID id, string name)> GetOnlineFriends()
|
||
{
|
||
var list = new List<(CSteamID, string)>();
|
||
int count = SteamFriends.GetFriendCount(EFriendFlags.k_EFriendFlagImmediate);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var fid = SteamFriends.GetFriendByIndex(i, EFriendFlags.k_EFriendFlagImmediate);
|
||
var state = SteamFriends.GetFriendPersonaState(fid);
|
||
if (state == EPersonaState.k_EPersonaStateOnline ||
|
||
state == EPersonaState.k_EPersonaStateAway ||
|
||
state == EPersonaState.k_EPersonaStateBusy ||
|
||
state == EPersonaState.k_EPersonaStateSnooze)
|
||
{
|
||
list.Add((fid, SteamFriends.GetFriendPersonaName(fid)));
|
||
}
|
||
}
|
||
return list;
|
||
}
|
||
|
||
// 获取所有在线好友信息
|
||
public void SetGameState(int state)
|
||
{
|
||
if (!IsInLobby() || !IsLobbyOwner()) return;
|
||
if (!CurrentLobby.IsValid()) return;
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "GameState", state.ToString());
|
||
}
|
||
|
||
public Dictionary<ulong, MemberInfo> GetOnlineFriendsDict()
|
||
{
|
||
return _onlineFriendsInfo;
|
||
}
|
||
|
||
// 设置房间数据
|
||
public void SetLobbyData(string key, string value)
|
||
{
|
||
if (!IsLobbyOwner())
|
||
{
|
||
LogSystem.LogError("Only lobby owner can set lobby data");
|
||
return;
|
||
}
|
||
|
||
if (!CurrentLobby.IsValid()) return;
|
||
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, key, value);
|
||
}
|
||
|
||
// 获取房间数据
|
||
public string GetLobbyData(string key)
|
||
{
|
||
if (!CurrentLobby.IsValid()) return "";
|
||
return SteamMatchmaking.GetLobbyData(CurrentLobby, key);
|
||
}
|
||
|
||
// 房间创建回调
|
||
private void OnLobbyCreatedCallback(LobbyCreated_t data)
|
||
{
|
||
if (data.m_eResult != EResult.k_EResultOK)
|
||
{
|
||
CurrentState = LobbyState.None;
|
||
LogSystem.LogError($"Failed to create lobby: {data.m_eResult}");
|
||
return;
|
||
}
|
||
|
||
CurrentLobby = new CSteamID(data.m_ulSteamIDLobby);
|
||
CachedOwner = SteamUser.GetSteamID();
|
||
|
||
LogSystem.LogInfo($"Lobby created successfully: {CurrentLobby}");
|
||
|
||
// 设置房间基础数据
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "Game", "TOHOTOPIA");
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "Owner", SelfName);
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "Version", ConfigManager.Instance.VersionCfg.CurVersionInfo.Version);
|
||
//SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", FilterRoomName(SelfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix)));
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", SelfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix));
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomCode", GenerateRoomCode());
|
||
SteamMatchmaking.SetLobbyData(CurrentLobby, "GameState", "0");
|
||
|
||
// 设置Rich Presence
|
||
SteamFriends.SetRichPresence("status", "In Lobby");
|
||
|
||
OnLobbyCreatedEvent?.Invoke(CurrentLobby);
|
||
}
|
||
|
||
private void OnSteamServersConnectedCallback(SteamServersConnected_t data)
|
||
{
|
||
_steamServerConnected = true;
|
||
_steamSessionFailCount = 0;
|
||
LogSystem.LogInfo("Steam servers connected");
|
||
}
|
||
|
||
private void OnSteamServersDisconnectedCallback(SteamServersDisconnected_t data)
|
||
{
|
||
_steamServerConnected = false;
|
||
_steamSessionFailCount = SteamSessionFailThreshold;
|
||
HandleLocalSteamSessionLost($"Steam servers disconnected: {data.m_eResult}");
|
||
}
|
||
|
||
private void OnSteamServerConnectFailureCallback(SteamServerConnectFailure_t data)
|
||
{
|
||
_steamServerConnected = false;
|
||
LogSystem.LogWarning($"Steam server connect failure: {data.m_eResult}");
|
||
}
|
||
|
||
private void OnSteamShutdownCallback(SteamShutdown_t data)
|
||
{
|
||
_steamServerConnected = false;
|
||
_steamSessionFailCount = SteamSessionFailThreshold;
|
||
HandleLocalSteamSessionLost("Steam shutdown callback");
|
||
}
|
||
|
||
// 加入请求回调
|
||
private void OnLobbyJoinRequestedCallback(GameLobbyJoinRequested_t data)
|
||
{
|
||
LogSystem.LogInfo($"Join requested for lobby: {data.m_steamIDLobby}");
|
||
|
||
// 检查是否被踢或房间被解散
|
||
var disbandedData = SteamMatchmaking.GetLobbyData(data.m_steamIDLobby, "disbanded");
|
||
if (disbandedData == "true")
|
||
{
|
||
LogSystem.LogInfo($"Cannot join disbanded lobby");
|
||
return;
|
||
}
|
||
|
||
JoinLobby(data.m_steamIDLobby);
|
||
}
|
||
|
||
// 进入房间回调
|
||
private void OnLobbyEnterCallback(LobbyEnter_t data)
|
||
{
|
||
// 检查加入结果
|
||
if ((EChatRoomEnterResponse)data.m_EChatRoomEnterResponse != EChatRoomEnterResponse.k_EChatRoomEnterResponseSuccess)
|
||
{
|
||
CurrentState = LobbyState.None;
|
||
var errorMsg = $"Failed to enter lobby: {(EChatRoomEnterResponse)data.m_EChatRoomEnterResponse}";
|
||
LogSystem.LogError(errorMsg);
|
||
OnLobbyErrorEvent?.Invoke(errorMsg);
|
||
return;
|
||
}
|
||
|
||
CurrentLobby = new CSteamID(data.m_ulSteamIDLobby);
|
||
CachedOwner = SteamMatchmaking.GetLobbyOwner(CurrentLobby);
|
||
CurrentState = LobbyState.InLobby;
|
||
_isHandlingSessionLoss = false;
|
||
_steamSessionFailCount = 0;
|
||
|
||
LogSystem.LogInfo($"Successfully entered lobby: {CurrentLobby}");
|
||
|
||
// 检查是否被踢
|
||
CheckIfKicked();
|
||
OnLobbyReadyInternal();
|
||
OnLobbyMembersChangedInternal();
|
||
if (IsLobbyOwner())
|
||
{
|
||
GameNetSender.Instance.ClearLobbyDataSyncState();
|
||
}
|
||
else
|
||
{
|
||
GameNetSender.Instance.MarkLobbyDataSyncRequired();
|
||
GameNetSender.Instance.RequestLobbyData(true);
|
||
}
|
||
|
||
// 触发加入成功事件
|
||
OnLobbyEnteredEvent?.Invoke(CurrentLobby);
|
||
}
|
||
|
||
// 成员变化回调
|
||
private void OnLobbyChatUpdateCallback(LobbyChatUpdate_t data)
|
||
{
|
||
if (data.m_ulSteamIDLobby != CurrentLobby.m_SteamID) return;
|
||
|
||
var changedUser = new CSteamID(data.m_ulSteamIDUserChanged);
|
||
var stateChange = (EChatMemberStateChange)data.m_rgfChatMemberStateChange;
|
||
|
||
LogSystem.LogInfo($"Lobby member update: {changedUser} - {stateChange}");
|
||
|
||
// 处理成员状态变化
|
||
if (stateChange.HasFlag(EChatMemberStateChange.k_EChatMemberStateChangeEntered))
|
||
{
|
||
OnMemberJoinedEvent?.Invoke(changedUser);
|
||
}
|
||
else if (stateChange.HasFlag(EChatMemberStateChange.k_EChatMemberStateChangeLeft) ||
|
||
stateChange.HasFlag(EChatMemberStateChange.k_EChatMemberStateChangeDisconnected) ||
|
||
stateChange.HasFlag(EChatMemberStateChange.k_EChatMemberStateChangeKicked) ||
|
||
stateChange.HasFlag(EChatMemberStateChange.k_EChatMemberStateChangeBanned))
|
||
{
|
||
OnMemberLeftEvent?.Invoke(changedUser);
|
||
|
||
// 断开P2P连接
|
||
SimpleP2P.Instance.DisconnectFromPeer(changedUser);
|
||
}
|
||
|
||
OnLobbyMembersChangedInternal();
|
||
CheckOwnerChange();
|
||
}
|
||
|
||
// 检查房主变化
|
||
private void CheckOwnerChange()
|
||
{
|
||
if (!CurrentLobby.IsValid()) return;
|
||
|
||
var currentOwner = SteamMatchmaking.GetLobbyOwner(CurrentLobby);
|
||
if (!CachedOwner.IsValid())
|
||
{
|
||
CachedOwner = currentOwner;
|
||
return;
|
||
}
|
||
|
||
if (currentOwner != CachedOwner)
|
||
{
|
||
var oldOwner = CachedOwner;
|
||
CachedOwner = currentOwner;
|
||
|
||
LogSystem.LogInfo($"Host changed from {oldOwner} to {currentOwner}");
|
||
|
||
OnHostChangedEvent?.Invoke(oldOwner, currentOwner);
|
||
OnHostChangedInternal(oldOwner, currentOwner);
|
||
}
|
||
}
|
||
|
||
// 房主切换时内部处理
|
||
private void OnHostChangedInternal(CSteamID oldOwner, CSteamID newOwner)
|
||
{
|
||
if (oldOwner == SteamUser.GetSteamID())
|
||
{
|
||
LogSystem.LogWarning($"Local host ownership changed to {newOwner}, waiting local disconnect handling");
|
||
return;
|
||
}
|
||
|
||
LogSystem.LogWarning($"Host left lobby(oldHost={oldOwner}, newHost={newOwner}), forcing all members leave");
|
||
HandleLocalSteamSessionLost($"Host changed from {oldOwner} to {newOwner}");
|
||
}
|
||
|
||
// 房间准备完毕时内部处理
|
||
private void OnLobbyReadyInternal()
|
||
{
|
||
CheckConnectionStatus();
|
||
}
|
||
|
||
// 检查连接状态
|
||
public void CheckConnectionStatus()
|
||
{
|
||
// 如果不是房主,连接到房主
|
||
if (!IsLobbyOwner() && CachedOwner.IsValid() && !SimpleP2P.Instance.IsConnectedTo(CachedOwner))
|
||
{
|
||
LogSystem.LogInfo($"Reconnecting to host: {CachedOwner}");
|
||
SimpleP2P.Instance.ConnectToPeer(CachedOwner);
|
||
}
|
||
}
|
||
|
||
// 有成员变化时内部处理
|
||
private void OnLobbyMembersChangedInternal()
|
||
{
|
||
var members = EnumerateMembers().ToList();
|
||
LogSystem.LogInfo($"Lobby members changed. Count: {members.Count}");
|
||
UpdateMemberInfo();
|
||
OnMembersChangedEvent?.Invoke(members);
|
||
}
|
||
|
||
// 检查是否被踢
|
||
private void CheckIfKicked()
|
||
{
|
||
var kickData = SteamMatchmaking.GetLobbyMemberData(CurrentLobby, SteamUser.GetSteamID(), "kicked");
|
||
var specificKick = SteamMatchmaking.GetLobbyData(CurrentLobby, $"kick_{SteamUser.GetSteamID().m_SteamID}");
|
||
|
||
if (kickData == "true" || specificKick == "true")
|
||
{
|
||
LogSystem.LogInfo("I was kicked from the lobby");
|
||
LeaveLobby();
|
||
}
|
||
}
|
||
|
||
// 重置房间状态
|
||
private void ResetLobbyState()
|
||
{
|
||
CurrentLobby = CSteamID.Nil;
|
||
CachedOwner = CSteamID.Nil;
|
||
CurrentState = LobbyState.None;
|
||
_isHandlingSessionLoss = false;
|
||
_steamSessionFailCount = 0;
|
||
_refreshSteamStatus = 0f;
|
||
|
||
// 清除Rich Presence
|
||
SteamFriends.ClearRichPresence();
|
||
}
|
||
|
||
// P2P事件处理
|
||
private void OnP2PPeerConnected(CSteamID steamID)
|
||
{
|
||
if (IsLobbyOwner())
|
||
{
|
||
Main.Instance.GameLogic.OnConnectToOtherPlayer(steamID.m_SteamID);
|
||
if (Main.Instance.GameLogic.GetCurState() == GameState.Menu && Main.Instance.MapConfig != null)
|
||
{
|
||
if (Main.Instance.MapConfig.UpdateLobbyMember(GetAllMemberInfo()))
|
||
Main.Instance.MapConfig.CheckMapConfigChanged();
|
||
GameNetSender.Instance.SendLobbyData(Main.Instance.MapConfig, steamID.m_SteamID);
|
||
}
|
||
}
|
||
else if (steamID.m_SteamID == GetLobbyOwnerId())
|
||
GameNetSender.Instance.RequestLobbyData(true);
|
||
LogSystem.LogInfo($"P2P connection established with: {steamID}");
|
||
}
|
||
|
||
private void OnP2PPeerDisconnected(CSteamID steamID)
|
||
{
|
||
if (!IsLobbyOwner())
|
||
{
|
||
if (steamID.m_SteamID == GetLobbyOwnerId()) Main.Instance.GameLogic.OnDisconnectToHost();
|
||
}
|
||
LogSystem.LogInfo($"P2P connection lost with: {steamID}");
|
||
}
|
||
|
||
private void OnP2PConnectionError(string error)
|
||
{
|
||
LogSystem.LogError($"P2P connection error: {error}");
|
||
}
|
||
|
||
private void OnP2PMessageSendFailed(CSteamID steamID, string reason)
|
||
{
|
||
var error = $"P2P message send failed: target={steamID}, reason={reason}";
|
||
LogSystem.LogError(error);
|
||
if (_suppressP2PSendFailureLobbyErrors <= 0) OnLobbyErrorEvent?.Invoke(error);
|
||
}
|
||
|
||
// 发送P2P消息
|
||
public bool SendMessageToPeer(ulong member, byte[] data, bool reliable = true)
|
||
{
|
||
if (!IsInLobby())
|
||
{
|
||
return ReportP2PSendPrecheckFailed(member, "Not in lobby");
|
||
}
|
||
|
||
if (data == null || data.Length == 0)
|
||
return ReportP2PSendPrecheckFailed(member, "Trying to send null or empty data");
|
||
if (!IsMemberInLobby(member))
|
||
return ReportP2PSendPrecheckFailed(member, $"Target member is not in lobby: {member}");
|
||
if (member == GetSelfMemberId())
|
||
return ReportP2PSendPrecheckFailed(member, $"Trying to send P2P message to self: {member}");
|
||
var cSteamId = new CSteamID(member);
|
||
return SimpleP2P.Instance.SendTo(cSteamId, data, reliable);
|
||
}
|
||
|
||
// 广播P2P消息
|
||
public bool BroadcastMessage(byte[] data, bool reliable = true)
|
||
{
|
||
if (!IsInLobby())
|
||
{
|
||
OnLobbyErrorEvent?.Invoke("P2P broadcast failed: Not in lobby");
|
||
LogSystem.LogError("P2P broadcast failed: Not in lobby");
|
||
return false;
|
||
}
|
||
|
||
if (data == null || data.Length == 0)
|
||
{
|
||
OnLobbyErrorEvent?.Invoke("P2P broadcast failed: Trying to broadcast null or empty data");
|
||
LogSystem.LogError("P2P broadcast failed: Trying to broadcast null or empty data");
|
||
return false;
|
||
}
|
||
|
||
var selfMemberId = GetSelfMemberId();
|
||
var targets = new List<CSteamID>();
|
||
foreach (var memberId in GetAllMemberIds())
|
||
{
|
||
if (memberId == selfMemberId) continue;
|
||
targets.Add(new CSteamID(memberId));
|
||
}
|
||
|
||
if (targets.Count == 0) return true;
|
||
var successCount = 0;
|
||
var failedCount = 0;
|
||
_suppressP2PSendFailureLobbyErrors++;
|
||
try
|
||
{
|
||
foreach (var target in targets)
|
||
{
|
||
if (SimpleP2P.Instance.SendTo(target, data, reliable))
|
||
{
|
||
successCount++;
|
||
continue;
|
||
}
|
||
|
||
failedCount++;
|
||
LogSystem.LogWarning($"P2P broadcast enqueue failed: target={target}, bytes={data.Length}");
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
_suppressP2PSendFailureLobbyErrors--;
|
||
}
|
||
|
||
if (successCount > 0)
|
||
{
|
||
if (failedCount > 0)
|
||
LogSystem.LogWarning($"P2P broadcast partially succeeded: success={successCount}, failed={failedCount}, bytes={data.Length}");
|
||
return true;
|
||
}
|
||
|
||
var error = $"P2P broadcast failed for all targets: targets={targets.Count}, bytes={data.Length}";
|
||
LogSystem.LogError(error);
|
||
OnLobbyErrorEvent?.Invoke(error);
|
||
return false;
|
||
}
|
||
|
||
private bool ReportP2PSendPrecheckFailed(ulong member, string reason)
|
||
{
|
||
OnP2PMessageSendFailed(new CSteamID(member), reason);
|
||
return false;
|
||
}
|
||
|
||
// 枚举房间成员
|
||
private IEnumerable<CSteamID> EnumerateMembers()
|
||
{
|
||
if (!CurrentLobby.IsValid()) yield break;
|
||
int count = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
|
||
for (int i = 0; i < count; i++)
|
||
yield return SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
|
||
}
|
||
|
||
// 获取房间所有成员
|
||
public List<ulong> GetAllMemberIds()
|
||
{
|
||
return _memberInfos.Keys.ToList();
|
||
}
|
||
|
||
// 刷新房间内的成员信息
|
||
private void UpdateMemberInfo()
|
||
{
|
||
if (!CurrentLobby.IsValid()) return;
|
||
_memberInfos.Clear();
|
||
int count = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var cSteamId = SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
|
||
if (!_membersCache.ContainsKey(cSteamId.m_SteamID)) _membersCache[cSteamId.m_SteamID] = cSteamId;
|
||
_memberInfos[cSteamId.m_SteamID] = new MemberInfo();
|
||
_memberInfos[cSteamId.m_SteamID].Id = cSteamId.m_SteamID;
|
||
_memberInfos[cSteamId.m_SteamID].Name = SteamFriends.GetFriendPersonaName(cSteamId);
|
||
_memberInfos[cSteamId.m_SteamID].Texture = GetMemberAvatar(cSteamId.m_SteamID);
|
||
}
|
||
}
|
||
|
||
public Dictionary<ulong, MemberInfo> GetAllMemberInfo()
|
||
{
|
||
return _memberInfos;
|
||
}
|
||
|
||
public MemberInfo GetMemberInfo(ulong steamID)
|
||
{
|
||
return _memberInfos.GetValueOrDefault(steamID);
|
||
}
|
||
|
||
// 获取头像
|
||
public Texture2D GetMemberAvatar(ulong memberId)
|
||
{
|
||
var cSteamId = GetCSteamID(memberId);
|
||
if (!cSteamId.IsValid()) return null;
|
||
|
||
// 先检查头像是否可用
|
||
if (SteamFriends.RequestUserInformation(cSteamId, false))
|
||
{
|
||
// 如果返回false,说明信息已缓存,可以直接获取
|
||
// 如果返回true,说明正在请求,需要等待
|
||
LogSystem.LogInfo($"Avatar for {memberId} is being downloaded");
|
||
return null;
|
||
}
|
||
|
||
int avatarInt = SteamFriends.GetLargeFriendAvatar(cSteamId);
|
||
if (avatarInt == -1 || avatarInt == 0) // 0也表示无效
|
||
{
|
||
LogSystem.LogWarning($"Invalid avatar handle for {memberId}: {avatarInt}");
|
||
return null;
|
||
}
|
||
bool success = SteamUtils.GetImageSize(avatarInt, out uint width, out uint height);
|
||
if (!success || width == 0 || height == 0)
|
||
{
|
||
LogSystem.LogWarning($"Invalid avatar size for {memberId}: {width}x{height}");
|
||
return null;
|
||
}
|
||
|
||
byte[] imageData = new byte[width * height * 4];
|
||
success = SteamUtils.GetImageRGBA(avatarInt, imageData, imageData.Length);
|
||
if (!success)
|
||
{
|
||
LogSystem.LogWarning($"Failed to get avatar data for {memberId}");
|
||
return null;
|
||
}
|
||
|
||
Texture2D avatar = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false);
|
||
avatar.LoadRawTextureData(imageData);
|
||
avatar.Apply();
|
||
return avatar;
|
||
}
|
||
|
||
// 获取成员数量
|
||
public int GetMemberCount()
|
||
{
|
||
if (!CurrentLobby.IsValid()) return 0;
|
||
return SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
|
||
}
|
||
|
||
// 获取最大成员数
|
||
public int GetMemberLimit()
|
||
{
|
||
if (!CurrentLobby.IsValid()) return 0;
|
||
return SteamMatchmaking.GetLobbyMemberLimit(CurrentLobby);
|
||
}
|
||
|
||
// 判断成员是否在房间中
|
||
public bool IsMemberInLobby(ulong memberId)
|
||
{
|
||
if (!CurrentLobby.IsValid()) return false;
|
||
int count = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var cSteamId = SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
|
||
if (cSteamId.m_SteamID == memberId) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 根据 memberId 获取 CSteamId
|
||
public CSteamID GetCSteamID(ulong memberId)
|
||
{
|
||
if (!_membersCache.ContainsKey(memberId)) return CSteamID.Nil;
|
||
return _membersCache[memberId];
|
||
}
|
||
|
||
// 获取自己的 memberId
|
||
public ulong GetSelfMemberId()
|
||
{
|
||
return SteamUser.GetSteamID().m_SteamID;
|
||
}
|
||
|
||
// 获取房主的 memberId
|
||
public ulong GetLobbyOwnerId()
|
||
{
|
||
if (!CurrentLobby.IsValid()) return 0;
|
||
var owner = SteamMatchmaking.GetLobbyOwner(CurrentLobby);
|
||
return owner.m_SteamID;
|
||
}
|
||
|
||
// 获取当前房间状态
|
||
public LobbyState GetCurState()
|
||
{
|
||
return CurrentState;
|
||
}
|
||
|
||
// 自己是否是房主
|
||
public bool IsLobbyOwner()
|
||
{
|
||
return IsInLobby() && SteamMatchmaking.GetLobbyOwner(CurrentLobby) == SteamUser.GetSteamID();
|
||
}
|
||
|
||
// 自己是否在房间中
|
||
public bool IsInLobby()
|
||
{
|
||
return IsInitialized() && CurrentState == LobbyState.InLobby && CurrentLobby.IsValid();
|
||
}
|
||
|
||
// 获取自己的当前Steam在线状态 (如: 在线, 忙碌, 离开, 隐身, 离线)
|
||
public EPersonaState GetSelfPersonaState()
|
||
{
|
||
if (!_isSteamInitialized || !_isLoggedIn) return EPersonaState.k_EPersonaStateOffline;
|
||
return SteamFriends.GetPersonaState();
|
||
}
|
||
|
||
// 检查自己当前的Steam在线状态是否为“隐身”或“离线”
|
||
// 重要提醒:Steam客户端API【无法直接获取】玩家个人资料面板上设置的“资料页隐私(公开/仅好友/私密)”或“游戏详情隐私”。
|
||
// 资料页的隐私设置是受到Steam严格保护的,如果玩家资料设置了私密导致别人看不见加入按钮,代码层面是查不到的。
|
||
// 只能通过在UI上写一行提示:“如果您邀请的好友没有出现加入按钮,请将您的Steam资料或游戏详情设为公开”。
|
||
public bool IsSelfStatusInvisibleOrOffline()
|
||
{
|
||
var state = GetSelfPersonaState();
|
||
return state == EPersonaState.k_EPersonaStateOffline ||
|
||
state == EPersonaState.k_EPersonaStateInvisible;
|
||
}
|
||
|
||
// 清理资源
|
||
public void Cleanup()
|
||
{
|
||
LeaveLobby();
|
||
SimpleP2P.Instance.OnPeerConnectedEvent -= OnP2PPeerConnected;
|
||
SimpleP2P.Instance.OnPeerDisconnectedEvent -= OnP2PPeerDisconnected;
|
||
SimpleP2P.Instance.OnConnectionErrorEvent -= OnP2PConnectionError;
|
||
SimpleP2P.Instance.OnMessageSendFailedEvent -= OnP2PMessageSendFailed;
|
||
SimpleP2P.Instance.Cleanup();
|
||
|
||
_cbLobbyCreated?.Dispose();
|
||
_cbLobbyJoinRequested?.Dispose();
|
||
_cbLobbyEnter?.Dispose();
|
||
_cbLobbyChatUpdate?.Dispose();
|
||
_cbSteamServersConnected?.Dispose();
|
||
_cbSteamServersDisconnected?.Dispose();
|
||
_cbSteamServerConnectFailure?.Dispose();
|
||
_cbSteamShutdown?.Dispose();
|
||
|
||
LogSystem.LogInfo("SteamLobbyManager cleaned up");
|
||
}
|
||
}
|
||
|
||
|
||
// 房间信息结构
|
||
[MemoryPackable]
|
||
public partial class LobbyListInfo
|
||
{
|
||
public ulong LobbyId;
|
||
public ulong OwnerId;
|
||
|
||
public string OwnerName;
|
||
public string RoomName;
|
||
public string Version;
|
||
public int CurrentPlayers;
|
||
public int MaxPlayers;
|
||
public int GameState;
|
||
}
|
||
}
|