TH1/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs
2026-06-05 11:56:51 +08:00

2148 lines
91 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 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.Chat;
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
{
public const string LobbyPasswordWrongError = "LobbyPasswordWrong";
// 登录状态
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 bool _steamApiUnavailable;
private bool _steamApiUnavailableLogged;
private int _suppressP2PSendFailureLobbyErrors;
private const float SteamSessionCheckInterval = 1f;
private const int SteamSessionFailThreshold = 2;
private const string LobbyHasPasswordKey = "HasPassword";
private const string LobbyPasswordKey = "Password";
private const string LobbyIsPublicKey = "IsPublic";
private const string LobbyOwnerSteamIdKey = "OwnerSteamId";
private const string LobbyReportCountKey = "ReportCount";
private const string LobbyRoomNameKey = "RoomName";
private const string LobbyGameStateKey = "GameState";
private const string LobbyKickKeyPrefix = "kick_";
private const int LobbyReportRenameThreshold = 5;
private const string ReportedLobbyDefaultRoomName = "Default";
private string _pendingLobbyPassword = "";
private string _pendingLobbyRoomName = "";
private bool _pendingLobbyIsPublic = true;
private CSteamID _pendingJoinLobby = CSteamID.Nil;
private string _pendingJoinPassword = "";
private bool _pendingJoinCheckPassword = true;
private string RoomName;
// 房间列表
private List<LobbyListInfo> _lobbyListInfos;
public List<LobbyListInfo> LobbyListInfos => _lobbyListInfos;
private readonly HashSet<ulong> _lobbyReporters = new HashSet<ulong>();
// 事件委托
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 = 目标 Lobbydata.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<LobbyDataUpdate_t> _cbLobbyDataUpdate;
// 搜索公开房间
private Callback<LobbyMatchList_t> _cbLobbyMatchList;
public SteamLobbyManager()
{
_steamSDKUpdateRecord = 2;
_onlineFriendsIdUpdateRecord = 2;
_kickInfoUpdateRecord = 2;
_refreshSteamStatus = 0;
_steamSessionFailCount = 0;
_isHandlingSessionLoss = false;
_steamApiUnavailable = false;
_steamApiUnavailableLogged = 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();
}
private bool TrySteamApi<T>(string context, Func<T> action, out T value)
{
value = default;
if (_steamApiUnavailable) return false;
try
{
value = action();
return true;
}
catch (DllNotFoundException e)
{
MarkSteamApiUnavailable(context, e);
return false;
}
catch (EntryPointNotFoundException e)
{
MarkSteamApiUnavailable(context, e);
return false;
}
catch (InvalidOperationException e)
{
MarkSteamApiUnavailable(context, e);
return false;
}
catch (Exception e)
{
LogSystem.LogWarning($"Steam API call failed at {context}: {e.Message}");
return false;
}
}
private bool TrySteamApi(string context, System.Action action)
{
if (_steamApiUnavailable) return false;
try
{
action();
return true;
}
catch (DllNotFoundException e)
{
MarkSteamApiUnavailable(context, e);
return false;
}
catch (EntryPointNotFoundException e)
{
MarkSteamApiUnavailable(context, e);
return false;
}
catch (InvalidOperationException e)
{
MarkSteamApiUnavailable(context, e);
return false;
}
catch (Exception e)
{
LogSystem.LogWarning($"Steam API call failed at {context}: {e.Message}");
return false;
}
}
private void MarkSteamApiUnavailable(string context, Exception e)
{
_steamApiUnavailable = true;
_isSteamInitialized = false;
_isLoggedIn = false;
_isLobbyInitialized = false;
_steamServerConnected = false;
_status = new SteamNetworkStatus();
if (!_steamApiUnavailableLogged)
{
_steamApiUnavailableLogged = true;
LogSystem.LogWarning($"Steam API unavailable at {context}: {e.GetType().Name}: {e.Message}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamUnavailable);
}
}
private bool EnsureSteamReadyForLobbyAction(string context)
{
if (_steamApiUnavailable || !_isSteamInitialized)
{
LogSystem.LogWarning($"Steam lobby action skipped because Steam API is unavailable: {context}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamUnavailable);
return false;
}
return true;
}
// 初始化
public void Init()
{
if (_steamApiUnavailable) return;
_steamSDKUpdateRecord += Time.deltaTime;
if (_steamSDKUpdateRecord < 2) return;
_steamSDKUpdateRecord = 0;
RefreshSteamInit();
RefreshSteamStatus();
//RefreshSteamGUI();
RefreshLoginStatus();
RefreshLobbyStatus();
}
// 刷新 Steam
private void RefreshSteamInit()
{
if (_steamApiUnavailable) return;
if (_isSteamInitialized) return;
// 检查Steam是否运行
if (!TrySteamApi("SteamAPI.IsSteamRunning", SteamAPI.IsSteamRunning, out var steamRunning) || !steamRunning)
{
// LogSystem.LogError("Steam客户端未运行请先启动Steam。");
return;
}
// 初始化Steam API
LogSystem.LogInfo("开始初始化Steam...");
ESteamAPIInitResult initResult;
string steamErrMsg;
try
{
initResult = SteamAPI.InitEx(out steamErrMsg);
}
catch (DllNotFoundException e)
{
MarkSteamApiUnavailable("SteamAPI.InitEx", e);
return;
}
catch (EntryPointNotFoundException e)
{
MarkSteamApiUnavailable("SteamAPI.InitEx", e);
return;
}
catch (InvalidOperationException e)
{
MarkSteamApiUnavailable("SteamAPI.InitEx", e);
return;
}
catch (Exception e)
{
LogSystem.LogWarning($"Steam API initialization failed: {e.Message}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamUnavailable);
return;
}
_isSteamInitialized = initResult == ESteamAPIInitResult.k_ESteamAPIInitResult_OK;
if (!_isSteamInitialized)
{
LogSystem.LogWarning($"Steam API初始化失败result={initResult}, msg={steamErrMsg}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamUnavailable);
switch (initResult)
{
case ESteamAPIInitResult.k_ESteamAPIInitResult_NoSteamClient:
LogSystem.LogWarning("→ Steam客户端未运行或未登录请先打开Steam并登录账号。");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamLoginRequired);
break;
case ESteamAPIInitResult.k_ESteamAPIInitResult_VersionMismatch:
LogSystem.LogWarning("→ Steam客户端版本过旧请在Steam中检查更新。");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamClientOutdated);
break;
case ESteamAPIInitResult.k_ESteamAPIInitResult_FailedGeneric:
LogSystem.LogWarning("→ 通用失败。常见原因当前Steam账号未拥有该AppID需在Steamworks后台授予权限或工作目录下steam_appid.txt位置错误或上一次进程未退干净。");
break;
}
return;
}
LogSystem.LogInfo("Steam API初始化成功");
// 显示启动信息
DisplayLaunchInfo();
CheckSteamAppIdFile();
}
// 刷新 Steam 状态
private void RefreshSteamStatus()
{
if (!_isSteamInitialized) return;
// 基础Steam连接状态
if (!TrySteamApi("RefreshSteamStatus.IsSteamRunning", SteamAPI.IsSteamRunning, out var steamRunning)) return;
if (!TrySteamApi("RefreshSteamStatus.BLoggedOn", SteamUser.BLoggedOn, out var loggedOn)) return;
_status.IsSteamConnected = steamRunning;
_status.IsLoggedOn = loggedOn;
_status.IsInLobby = IsInLobby();
_status.IsP2PReady = SimpleP2P.Instance?.IsInitialized ?? false;
_status.SteamServerConnected = IsSteamSessionLikelyAlive();
_steamServerConnected = _status.SteamServerConnected;
}
// 刷新用户登录状态
private void RefreshLoginStatus()
{
if (!_isSteamInitialized || _isLoggedIn) return;
if (!TrySteamApi("RefreshLoginStatus.BLoggedOn", SteamUser.BLoggedOn, out var loggedOn)) return;
_isLoggedIn = loggedOn;
if (_isLoggedIn)
{
if (!TrySteamApi("RefreshLoginStatus.GetSteamID", SteamUser.GetSteamID, out _selfID)) return;
if (!TrySteamApi("RefreshLoginStatus.GetPersonaName", SteamFriends.GetPersonaName, out _selfName)) return;
CrashSightManager.UpdateSteamUserId(_selfID.m_SteamID);
LogSystem.LogInfo($"Steam用户已登录: {_selfName} ({_selfID})");
}
else
{
LogSystem.LogWarning("Steam用户未登录");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamLoginRequired);
}
}
// 刷新 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);
_cbLobbyDataUpdate = Callback<LobbyDataUpdate_t>.Create(OnLobbyDataUpdateCallback);
// 初始化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();
var latestFriendIds = new Dictionary<ulong, CSteamID>();
var latestFriendInfos = new Dictionary<ulong, MemberInfo>();
foreach (var kv in friends)
{
if (!_membersCache.ContainsKey(kv.id.m_SteamID)) _membersCache[kv.id.m_SteamID] = kv.id;
latestFriendIds[kv.id.m_SteamID] = kv.id;
latestFriendInfos[kv.id.m_SteamID] = new MemberInfo
{
Id = kv.id.m_SteamID,
Name = kv.name,
Texture = _onlineFriendsInfo.TryGetValue(kv.id.m_SteamID, out var oldInfo)
? oldInfo.Texture
: GetMemberAvatar(kv.id.m_SteamID)
};
}
_onlineFriendsId = latestFriendIds;
_onlineFriendsInfo = latestFriendInfos;
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回调
if (!TrySteamApi("SteamAPI.RunCallbacks", SteamAPI.RunCallbacks)) return;
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}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamSessionLost);
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 (!TrySteamApi("IsSteamSessionLikelyAlive.IsSteamRunning", SteamAPI.IsSteamRunning, out var steamRunning) || !steamRunning) return false;
if (!TrySteamApi("IsSteamSessionLikelyAlive.BLoggedOn", SteamUser.BLoggedOn, out var loggedOn) || !loggedOn) return false;
ESteamNetworkingAvailability avail;
SteamRelayNetworkStatus_t details;
try
{
avail = SteamNetworkingUtils.GetRelayNetworkStatus(out details);
}
catch (DllNotFoundException e)
{
MarkSteamApiUnavailable("SteamNetworkingUtils.GetRelayNetworkStatus", e);
return false;
}
catch (EntryPointNotFoundException e)
{
MarkSteamApiUnavailable("SteamNetworkingUtils.GetRelayNetworkStatus", e);
return false;
}
catch (InvalidOperationException e)
{
MarkSteamApiUnavailable("SteamNetworkingUtils.GetRelayNetworkStatus", e);
return false;
}
catch (Exception e)
{
LogSystem.LogWarning($"Steam relay status check failed: {e.Message}");
return false;
}
bool relayOk = avail == ESteamNetworkingAvailability.k_ESteamNetworkingAvailability_Current
|| details.m_eAvail == ESteamNetworkingAvailability.k_ESteamNetworkingAvailability_Current;
return relayOk;
}
public bool CanCreateLobbyNow(out string reason)
{
reason = string.Empty;
if (!IsSteamSessionLikelyAlive())
{
reason = "Steam session is not alive.";
return false;
}
if (SimpleP2P.Instance == null || !SimpleP2P.Instance.IsInitialized)
{
reason = "P2P listen socket is not ready.";
return false;
}
if (SimpleP2P.Instance.HasRecentConnectionFailure(30f, out var p2pReason))
{
reason = p2pReason;
return false;
}
return true;
}
// 检查 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
if (!TrySteamApi("DisplayLaunchInfo.GetAppID", SteamUtils.GetAppID, out var currentAppId)) return;
LogSystem.LogInfo($"当前Steam App ID: {currentAppId}");
// 检查启动方式
if (!TrySteamApi("DisplayLaunchInfo.BIsSubscribedApp", () => SteamApps.BIsSubscribedApp(currentAppId), out var launchedViaSteam)) return;
LogSystem.LogInfo($"通过Steam启动: {(launchedViaSteam ? "" : "")}");
// 显示Steam环境信息
if (TrySteamApi("DisplayLaunchInfo.GetCurrentGameLanguage", SteamApps.GetCurrentGameLanguage, out var language))
LogSystem.LogInfo($"Steam语言: {language}");
if (TrySteamApi("DisplayLaunchInfo.BLoggedOn", SteamUser.BLoggedOn, out var loggedOn))
LogSystem.LogInfo($"Steam服务器连接: {(loggedOn ? "" : "")}");
// 检查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, string password = "", string roomName = "")
{
if (!EnsureSteamReadyForLobbyAction(nameof(CreateLobby))) return;
if (!CanCreateLobbyNow(out var reason))
{
LogSystem.LogWarning($"Cannot create lobby now: {reason}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
if (CurrentState != LobbyState.None)
{
LogSystem.LogInfo($"Cannot create lobby in state: {CurrentState}");
return;
}
_pendingLobbyPassword = password ?? "";
_pendingLobbyRoomName = FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetDefaultRoomName(SelfName) : roomName.Trim());
if (_pendingLobbyRoomName.Length < 2)
_pendingLobbyRoomName = FilterRoomName(GetDefaultRoomName(SelfName));
if (_pendingLobbyRoomName.Length < 2)
_pendingLobbyRoomName = "Default";
_pendingLobbyIsPublic = isPublic;
CurrentState = LobbyState.Creating;
LogSystem.LogInfo($"Creating {(isPublic ? "public" : "friends-only")} lobby with max members: {maxMembers}, hasPassword: {!string.IsNullOrEmpty(_pendingLobbyPassword)}");
if (!TrySteamApi("SteamMatchmaking.CreateLobby", () => SteamMatchmaking.CreateLobby(isPublic?ELobbyType.k_ELobbyTypePublic:ELobbyType.k_ELobbyTypeFriendsOnly, maxMembers)))
ResetLobbyState();
}
// 加入房间
public void JoinLobby(CSteamID lobbyId, string password = "")
{
JoinLobbyInternal(lobbyId, password, true, true);
}
private void JoinLobbyInternal(CSteamID lobbyId, string password, bool requestLobbyDataIfMissing, bool checkPassword)
{
if (!EnsureSteamReadyForLobbyAction(nameof(JoinLobby))) return;
if (CurrentState != LobbyState.None)
{
//LogSystem.LogInfo($"Cannot join lobby in state: {CurrentState}");
//return;
}
if (requestLobbyDataIfMissing && ShouldRequestLobbyDataBeforeJoin(lobbyId))
{
_pendingJoinLobby = lobbyId;
_pendingJoinPassword = password ?? "";
_pendingJoinCheckPassword = checkPassword;
if (!TrySteamApi("SteamMatchmaking.RequestLobbyData", () => SteamMatchmaking.RequestLobbyData(lobbyId), out var requestLobbyData)
|| !requestLobbyData)
{
ClearPendingJoinLobby();
LogSystem.LogWarning($"Failed to request lobby data before joining: {lobbyId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyJoinFailed);
OnLobbyErrorEvent?.Invoke("Failed to request lobby data before joining");
}
return;
}
if (checkPassword && !ValidateLobbyPasswordForJoin(lobbyId, password)) return;
// TODO 这里会涉及到房间子类对于UI的调用对于房间的多态是不合理的暂不处理糊屎
if (!TrySteamApi("SteamMatchmaking.GetLobbyData.Version", () => SteamMatchmaking.GetLobbyData(lobbyId, "Version"), out var version)) return;
if (!string.IsNullOrEmpty(version) && ConfigManager.Instance.VersionCfg.CurVersionInfo.Version != version)
{
LogSystem.LogInfo($"版本不一致 !!!");
// UI 弹框在这里
return;
}
LogSystem.LogInfo($"Joining lobby: {lobbyId}");
CurrentState = LobbyState.Joining;
if (!TrySteamApi("SteamMatchmaking.JoinLobby", () => SteamMatchmaking.JoinLobby(lobbyId)))
ResetLobbyState();
}
// 离开房间
public void LeaveLobby()
{
if (!CurrentLobby.IsValid() || CurrentState == LobbyState.None) return;
CurrentState = LobbyState.Leaving;
LogSystem.LogInfo("Leaving lobby");
// 断开所有P2P连接
SimpleP2P.Instance.DisconnectAll();
TrySteamApi("SteamMatchmaking.LeaveLobby", () => 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");
// 设置房间数据标记解散
TrySteamApi("SteamMatchmaking.SetLobbyData.disbanded", () => SteamMatchmaking.SetLobbyData(CurrentLobby, "disbanded", "true"));
// 踢出所有其他成员
foreach (var member in EnumerateMembers())
{
if (!TrySteamApi("DisbandLobby.GetSteamID", SteamUser.GetSteamID, out var selfId)) break;
if (member != selfId)
{
// Steam没有直接踢人API通过设置数据让客户端自动离开
TrySteamApi("SteamMatchmaking.SetLobbyMemberData.kicked", () => SteamMatchmaking.SetLobbyMemberData(CurrentLobby, "kicked", "true"));
}
}
// 房主最后离开
LeaveLobby();
}
// 踢出指定成员(仅房主可用)
public void KickMember(ulong memberId)
{
if (!IsLobbyOwner())
{
LogSystem.LogError("Only lobby owner can kick members");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
if (!TrySteamApi("KickMember.GetSteamID", SteamUser.GetSteamID, out var selfIdForKick)) return;
if (memberId == selfIdForKick.m_SteamID)
{
LogSystem.LogError("Cannot kick yourself");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
LogSystem.LogInfo($"Kicking member: {memberId}");
SimpleP2P.Instance.MarkExpectedDisconnect(new CSteamID(memberId));
// 房主写 LobbyData只有房主有权限设 LobbyData所有客户端都能 GetLobbyData 读到
// 之前用的是 SetLobbyMemberData那个 API 只能设自己的成员数据,写出去对方根本读不到 —— 协议不匹配
// 被踢方在 RefreshKickInfo → CheckIfKicked 里读 kick_{selfId},发现是 "true" 就自动 LeaveLobby
TrySteamApi("SteamMatchmaking.SetLobbyData.kick", () => SteamMatchmaking.SetLobbyData(CurrentLobby, GetKickKey(memberId), "true"));
}
// 通过房间ID直接加入无需好友关系
public void JoinLobbyById(ulong lobbyId, string password = "")
{
var lobbyCSteamId = new CSteamID(lobbyId);
JoinLobby(lobbyCSteamId, password);
}
public void JoinLobbyByInvite(ulong lobbyId)
{
var lobbyCSteamId = new CSteamID(lobbyId);
JoinLobbyInternal(lobbyCSteamId, "", true, false);
}
// 获取当前房间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, string password = "")
{
ulong lobbyId = Base36Decode(code);
if (lobbyId == 0)
{
LogSystem.LogError("Invalid room code");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyJoinFailed);
return;
}
JoinLobbyById(lobbyId, password);
}
private bool ValidateLobbyPasswordForJoin(CSteamID lobbyId, string password)
{
if (!LobbyIsPublic(lobbyId)) return true;
if (!LobbyHasPassword(lobbyId)) return true;
if (!TrySteamApi("ValidateLobbyPasswordForJoin.GetLobbyData", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyPasswordKey), out var expectedPassword)) return false;
if (!string.IsNullOrEmpty(expectedPassword) && (password ?? "") == expectedPassword) return true;
var errorMsg = string.IsNullOrEmpty(expectedPassword)
? "Lobby password data is missing"
: "Lobby password mismatch";
LogSystem.LogInfo(errorMsg);
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyJoinFailed);
OnLobbyErrorEvent?.Invoke(LobbyPasswordWrongError);
return false;
}
private bool ShouldRequestLobbyDataBeforeJoin(CSteamID lobbyId)
{
if (!lobbyId.IsValid()) return false;
if (!TrySteamApi("ShouldRequestLobbyDataBeforeJoin.Game", () => SteamMatchmaking.GetLobbyData(lobbyId, "Game"), out var game)) return false;
if (!TrySteamApi("ShouldRequestLobbyDataBeforeJoin.Version", () => SteamMatchmaking.GetLobbyData(lobbyId, "Version"), out var version)) return false;
if (!TrySteamApi("ShouldRequestLobbyDataBeforeJoin.HasPassword", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyHasPasswordKey), out var hasPassword)) return false;
if (!TrySteamApi("ShouldRequestLobbyDataBeforeJoin.Password", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyPasswordKey), out var lobbyPassword)) return false;
if (!TrySteamApi("ShouldRequestLobbyDataBeforeJoin.IsPublic", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyIsPublicKey), out var isPublic)) return false;
if (!string.IsNullOrEmpty(game)) return false;
if (!string.IsNullOrEmpty(version)) return false;
if (!string.IsNullOrEmpty(hasPassword)) return false;
if (!string.IsNullOrEmpty(lobbyPassword)) return false;
if (!string.IsNullOrEmpty(isPublic)) return false;
return true;
}
private void ClearPendingJoinLobby()
{
_pendingJoinLobby = CSteamID.Nil;
_pendingJoinPassword = "";
_pendingJoinCheckPassword = true;
}
private bool LobbyHasPassword(CSteamID lobbyId)
{
if (!TrySteamApi("LobbyHasPassword.HasPassword", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyHasPasswordKey), out var hasPasswordData)) return false;
if (IsTrueLobbyData(hasPasswordData)) return true;
if (IsFalseLobbyData(hasPasswordData)) return false;
return TrySteamApi("LobbyHasPassword.Password", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyPasswordKey), out var passwordData)
&& !string.IsNullOrEmpty(passwordData);
}
private bool LobbyIsPublic(CSteamID lobbyId)
{
if (!TrySteamApi("LobbyIsPublic.IsPublic", () => SteamMatchmaking.GetLobbyData(lobbyId, LobbyIsPublicKey), out var isPublicData)) return true;
if (IsTrueLobbyData(isPublicData)) return true;
if (IsFalseLobbyData(isPublicData)) return false;
return true;
}
private static bool IsTrueLobbyData(string value)
{
return value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private static bool IsFalseLobbyData(string value)
{
return value == "0" || string.Equals(value, "false", StringComparison.OrdinalIgnoreCase);
}
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;
var selfMemberId = GetSelfMemberId();
return selfMemberId == 0 ? null : Base36Encode(selfMemberId);
}
public void InviteByRawSteamId(string code)
{
if (!CurrentLobby.IsValid())
{
LogSystem.LogError("Not in a lobby");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
if (string.IsNullOrEmpty(code))
{
LogSystem.LogError("Invalid user code");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
ulong steamId = Base36Decode(code);
if (steamId == 0)
{
LogSystem.LogError("Invalid user code format");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
var targetId = new CSteamID(steamId);
if (!targetId.IsValid())
{
LogSystem.LogError("Invalid Steam ID");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
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");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (!CurrentLobby.IsValid())
{
LogSystem.LogError("Not in a lobby");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
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}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
}
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, LobbyRoomNameKey),
Version = SteamMatchmaking.GetLobbyData(CurrentLobby, "Version"),
CurrentPlayers = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby),
MaxPlayers = SteamMatchmaking.GetLobbyMemberLimit(CurrentLobby),
GameState = GetLobbyGameStateFromData(CurrentLobby),
HasPassword = LobbyHasPassword(CurrentLobby),
ReportCount = GetLobbyReportCountFromData(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}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
bool isSucceed = SimpleP2P.Instance.SendToWithOutConnect(targetId, bytes);
if (!isSucceed)
{
LogSystem.LogError($"Failed to send game invite to: {targetSteamId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
LogSystem.LogInfo($"Game invite sent to: {targetSteamId}");
return true;
}
public bool ReportLobby(LobbyListInfo lobbyInfo)
{
if (lobbyInfo == null || lobbyInfo.LobbyId == 0)
{
LogSystem.LogError("Report lobby failed: invalid lobby info");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (lobbyInfo.OwnerId == 0)
{
LogSystem.LogError($"Report lobby failed: missing owner Steam ID, lobby={lobbyInfo.LobbyId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
var selfId = GetSelfMemberId();
if (selfId == 0)
{
LogSystem.LogError("Report lobby failed: missing self Steam ID");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (lobbyInfo.OwnerId == selfId)
{
LogSystem.LogInfo($"Report lobby skipped: cannot report own lobby, lobby={lobbyInfo.LobbyId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
var targetId = new CSteamID(lobbyInfo.OwnerId);
if (!targetId.IsValid())
{
LogSystem.LogError($"Report lobby failed: invalid owner Steam ID, owner={lobbyInfo.OwnerId}, lobby={lobbyInfo.LobbyId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
var data = new LobbyReportMessage
{
LobbyId = lobbyInfo.LobbyId,
ReporterId = selfId,
Version = ConfigManager.Instance.VersionCfg.CurVersionInfo.Version,
};
var bytes = MemoryPackSerializer.Serialize<BaseMessage>(data);
if (!SimpleP2P.Instance.SendToWithOutConnect(targetId, bytes))
{
LogSystem.LogError($"Report lobby failed: send to owner failed, lobby={lobbyInfo.LobbyId}, owner={lobbyInfo.OwnerId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
LogSystem.LogInfo($"Report lobby sent: lobby={lobbyInfo.LobbyId}, owner={lobbyInfo.OwnerId}, reporter={selfId}");
return true;
}
public void OnReceivedLobbyReport(LobbyReportMessage message)
{
if (message == null) return;
if (!IsLobbyOwner()) return;
if (!CurrentLobby.IsValid() || message.LobbyId != CurrentLobby.m_SteamID)
{
LogSystem.LogWarning($"Ignore lobby report for unmatched lobby: current={CurrentLobby.m_SteamID}, report={message.LobbyId}");
return;
}
if (message.ReporterId == 0 || message.ReporterId == GetSelfMemberId())
{
LogSystem.LogWarning($"Ignore invalid lobby report: lobby={message.LobbyId}, reporter={message.ReporterId}");
return;
}
if (_lobbyReporters.Contains(message.ReporterId))
{
LogSystem.LogInfo($"Ignore duplicate lobby report: lobby={message.LobbyId}, reporter={message.ReporterId}");
return;
}
_lobbyReporters.Add(message.ReporterId);
var reportCount = GetLobbyReportCountFromData(CurrentLobby) + 1;
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyReportCountKey, reportCount.ToString());
LogSystem.LogInfo($"Lobby report received: lobby={message.LobbyId}, reporter={message.ReporterId}, count={reportCount}");
if (reportCount <= LobbyReportRenameThreshold) return;
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyRoomNameKey, ReportedLobbyDefaultRoomName);
RoomName = ReportedLobbyDefaultRoomName;
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
LogSystem.LogInfo($"Lobby report count exceeded threshold, room name reset: lobby={message.LobbyId}, count={reportCount}");
}
// 搜索房间
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 lobbyInfo = new LobbyListInfo { LobbyId = lobbyId.m_SteamID };
RefreshLobbyListInfo(lobbyInfo, lobbyId);
_lobbyListInfos.Add(lobbyInfo);
}
// 触发UI刷新事件
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
}
// 刷新房间信息
public void RefreshLobbyListInfo()
{
foreach (var lobbyInfo in _lobbyListInfos)
{
var cSteamId = new CSteamID(lobbyInfo.LobbyId);
RefreshLobbyListInfo(lobbyInfo, cSteamId);
}
}
private void RefreshLobbyListInfo(LobbyListInfo lobbyInfo, CSteamID lobbyId)
{
if (lobbyInfo == null) return;
lobbyInfo.LobbyId = lobbyId.m_SteamID;
lobbyInfo.OwnerId = GetLobbyOwnerSteamIdFromData(lobbyId);
lobbyInfo.OwnerName = SteamMatchmaking.GetLobbyData(lobbyId, "Owner");
lobbyInfo.RoomName = SteamMatchmaking.GetLobbyData(lobbyId, LobbyRoomNameKey);
lobbyInfo.Version = SteamMatchmaking.GetLobbyData(lobbyId, "Version");
lobbyInfo.CurrentPlayers = SteamMatchmaking.GetNumLobbyMembers(lobbyId);
lobbyInfo.MaxPlayers = SteamMatchmaking.GetLobbyMemberLimit(lobbyId);
lobbyInfo.GameState = GetLobbyGameStateFromData(lobbyId);
lobbyInfo.HasPassword = LobbyHasPassword(lobbyId);
lobbyInfo.ReportCount = GetLobbyReportCountFromData(lobbyId);
}
private static ulong GetLobbyOwnerSteamIdFromData(CSteamID lobbyId)
{
var ownerIdData = SteamMatchmaking.GetLobbyData(lobbyId, LobbyOwnerSteamIdKey);
return ulong.TryParse(ownerIdData, out var ownerId) ? ownerId : 0;
}
private static int GetLobbyReportCountFromData(CSteamID lobbyId)
{
var reportCountData = SteamMatchmaking.GetLobbyData(lobbyId, LobbyReportCountKey);
return int.TryParse(reportCountData, out var reportCount) && reportCount > 0 ? reportCount : 0;
}
private static int GetLobbyGameStateFromData(CSteamID lobbyId)
{
var gameStateData = SteamMatchmaking.GetLobbyData(lobbyId, LobbyGameStateKey);
return int.TryParse(gameStateData, out var gameState) ? gameState : 0;
}
public static string GetDefaultRoomName(string selfName)
{
return selfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix);
}
// 过滤房间名称,只保留中文、英文字母、数字和常用符号
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;
}
var filteredRoomName = result.ToString();
if (string.IsNullOrEmpty(filteredRoomName)) return "Default";
return BannedWordFilter.Filter(filteredRoomName, BannedTextContext.Name);
}
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)>();
if (!TrySteamApi("GetOnlineFriends.GetFriendCount", () => SteamFriends.GetFriendCount(EFriendFlags.k_EFriendFlagImmediate), out var count)) return list;
for (int i = 0; i < count; i++)
{
if (!TrySteamApi("GetOnlineFriends.GetFriendByIndex", () => SteamFriends.GetFriendByIndex(i, EFriendFlags.k_EFriendFlagImmediate), out var fid)) return list;
if (!TrySteamApi("GetOnlineFriends.GetFriendPersonaState", () => SteamFriends.GetFriendPersonaState(fid), out var state)) return list;
if (state == EPersonaState.k_EPersonaStateOnline ||
state == EPersonaState.k_EPersonaStateAway ||
state == EPersonaState.k_EPersonaStateBusy ||
state == EPersonaState.k_EPersonaStateSnooze)
{
if (!TrySteamApi("GetOnlineFriends.GetFriendPersonaName", () => SteamFriends.GetFriendPersonaName(fid), out var friendName)) return list;
list.Add((fid, friendName));
}
}
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");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return;
}
if (!CurrentLobby.IsValid()) return;
SteamMatchmaking.SetLobbyData(CurrentLobby, key, value);
}
// 获取房间数据
public string GetLobbyData(string key)
{
if (!CurrentLobby.IsValid()) return "";
return SteamMatchmaking.GetLobbyData(CurrentLobby, key);
}
public string GetRoomName()
{
var roomName = GetLobbyData("RoomName");
return FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetDefaultRoomName(SelfName) : roomName);
}
public bool SetRoomName(string roomName)
{
if (!IsLobbyOwner())
{
LogSystem.LogError("Only lobby owner can set room name");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (!CurrentLobby.IsValid()) return false;
roomName = FilterRoomName(string.IsNullOrWhiteSpace(roomName) ? GetRoomName() : roomName.Trim());
if (roomName.Length < 2)
roomName = FilterRoomName(GetRoomName());
if (roomName.Length < 2)
roomName = "Default";
bool success = SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", roomName);
if (success)
{
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
}
else
{
LogSystem.LogError($"Failed to set room name: {roomName}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
}
return success;
}
// 房间创建回调
private void OnLobbyCreatedCallback(LobbyCreated_t data)
{
if (data.m_eResult != EResult.k_EResultOK)
{
CurrentState = LobbyState.None;
_pendingLobbyPassword = "";
_pendingLobbyRoomName = "";
_pendingLobbyIsPublic = true;
LogSystem.LogError($"Failed to create lobby: {data.m_eResult}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyCreateFailed);
return;
}
CurrentLobby = new CSteamID(data.m_ulSteamIDLobby);
_lobbyReporters.Clear();
if (!TrySteamApi("OnLobbyCreatedCallback.GetSteamID", SteamUser.GetSteamID, out CachedOwner))
{
ResetLobbyState();
return;
}
LogSystem.LogInfo($"Lobby created successfully: {CurrentLobby}");
// 设置房间基础数据
SteamMatchmaking.SetLobbyData(CurrentLobby, "Game", "TOHOTOPIA");
SteamMatchmaking.SetLobbyData(CurrentLobby, "Owner", SelfName);
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyOwnerSteamIdKey, _selfID.m_SteamID.ToString());
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, LobbyRoomNameKey, SelfName + MultilingualManager.Instance.GetMultilingualText(Table.Instance.TextDataAssets.OutsideMultiplayRoomNameSuffix));
SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomName", _pendingLobbyRoomName);
SteamMatchmaking.SetLobbyData(CurrentLobby, "RoomCode", GenerateRoomCode());
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyGameStateKey, "0");
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyReportCountKey, "0");
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyHasPasswordKey, string.IsNullOrEmpty(_pendingLobbyPassword) ? "false" : "true");
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyPasswordKey, _pendingLobbyPassword);
SteamMatchmaking.SetLobbyData(CurrentLobby, LobbyIsPublicKey, _pendingLobbyIsPublic ? "true" : "false");
_pendingLobbyPassword = "";
_pendingLobbyRoomName = "";
_pendingLobbyIsPublic = true;
// 设置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}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.SteamSessionLost);
}
private void OnSteamShutdownCallback(SteamShutdown_t data)
{
_steamServerConnected = false;
_steamSessionFailCount = SteamSessionFailThreshold;
HandleLocalSteamSessionLost("Steam shutdown callback");
}
private void OnLobbyDataUpdateCallback(LobbyDataUpdate_t data)
{
var lobbyId = new CSteamID(data.m_ulSteamIDLobby);
if (data.m_bSuccess != 0 && CurrentLobby.IsValid() && lobbyId == CurrentLobby)
{
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
}
if (!_pendingJoinLobby.IsValid() || lobbyId != _pendingJoinLobby) return;
var password = _pendingJoinPassword;
var checkPassword = _pendingJoinCheckPassword;
ClearPendingJoinLobby();
if (data.m_bSuccess == 0)
{
LogSystem.LogError($"Failed to refresh lobby data before joining: {lobbyId}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyJoinFailed);
OnLobbyErrorEvent?.Invoke("Failed to refresh lobby data before joining");
return;
}
JoinLobbyInternal(lobbyId, password, false, checkPassword);
}
// 加入请求回调
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");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyJoinFailed);
return;
}
JoinLobbyInternal(data.m_steamIDLobby, "", true, false);
}
// 进入房间回调
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);
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyJoinFailed);
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(false);
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))
{
if (IsLobbyOwner()) ClearKickData(changedUser.m_SteamID);
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))
{
if (IsLobbyOwner()) ClearKickData(changedUser.m_SteamID);
OnMemberLeftEvent?.Invoke(changedUser);
Main.Instance?.GameLogic?.OnDisconnectToOtherPlayer(changedUser.m_SteamID, MemberNetState.Leaved);
// 断开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 (Main.Instance?.GameLogic != null && Main.Instance.GameLogic.GetCurState() == GameState.Menu)
{
LogSystem.LogInfo($"Lobby host transferred in menu(oldHost={oldOwner}, newHost={newOwner})");
CheckConnectionStatus();
ReconcilePreGameLobbyMembers();
return;
}
if (oldOwner.m_SteamID == GetSelfMemberId())
{
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");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.HostDisconnected);
HandleLocalSteamSessionLost($"Host changed from {oldOwner} to {newOwner}");
}
private void ReconcilePreGameLobbyMembers()
{
if (!IsInLobby()) return;
if (Main.Instance?.GameLogic == null || Main.Instance.GameLogic.GetCurState() != GameState.Menu) return;
if (Main.Instance.MapConfig == null) return;
if (IsLobbyOwner())
{
if (Main.Instance.MapConfig.UpdateLobbyMember(GetAllMemberInfo()))
Main.Instance.MapConfig.CheckMapConfigChanged();
else
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
GameNetSender.Instance.ClearLobbyDataSyncState();
return;
}
GameNetSender.Instance.MarkLobbyDataSyncRequired();
GameNetSender.Instance.RequestLobbyData(true);
EventManager.Publish(new UpdateUIOutsideMultiplayRoomSetting());
}
// 房间准备完毕时内部处理
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();
ReconcilePreGameLobbyMembers();
OnMembersChangedEvent?.Invoke(members);
}
// 检查是否被踢
private void CheckIfKicked(bool includeLobbyKickData = true)
{
if (!TrySteamApi("CheckIfKicked.GetSteamID", SteamUser.GetSteamID, out var selfId)) return;
if (!TrySteamApi("CheckIfKicked.GetLobbyMemberData", () => SteamMatchmaking.GetLobbyMemberData(CurrentLobby, selfId, "kicked"), out var kickData)) return;
if (!TrySteamApi("CheckIfKicked.GetLobbyData", () => SteamMatchmaking.GetLobbyData(CurrentLobby, GetKickKey(selfId.m_SteamID)), out var specificKick)) return;
if (kickData == "true" || includeLobbyKickData && specificKick == "true")
{
LogSystem.LogInfo("I was kicked from the lobby");
LeaveLobby();
}
}
private static string GetKickKey(ulong memberId)
{
return $"{LobbyKickKeyPrefix}{memberId}";
}
private void ClearKickData(ulong memberId)
{
if (!IsLobbyOwner() || !CurrentLobby.IsValid() || memberId == 0) return;
TrySteamApi("SteamMatchmaking.ClearLobbyData.kick", () => SteamMatchmaking.SetLobbyData(CurrentLobby, GetKickKey(memberId), ""));
}
// 重置房间状态
private void ResetLobbyState()
{
CurrentLobby = CSteamID.Nil;
CachedOwner = CSteamID.Nil;
CurrentState = LobbyState.None;
_isHandlingSessionLoss = false;
_steamSessionFailCount = 0;
_refreshSteamStatus = 0f;
_pendingLobbyPassword = "";
_pendingLobbyRoomName = "";
_pendingLobbyIsPublic = true;
ClearPendingJoinLobby();
_lobbyReporters.Clear();
// 清除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();
}
else
{
var state = IsMemberInLobby(steamID.m_SteamID) ? MemberNetState.Disconnected : MemberNetState.Leaved;
Main.Instance.GameLogic.OnDisconnectToOtherPlayer(steamID.m_SteamID, state);
}
LogSystem.LogInfo($"P2P connection lost with: {steamID}");
}
private void OnP2PConnectionError(string error)
{
LogSystem.LogError($"P2P connection error: {error}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PConnectionFailed, error);
}
private void OnP2PMessageSendFailed(CSteamID steamID, string reason)
{
var error = $"P2P message send failed: target={steamID}, reason={reason}";
LogSystem.LogError(error);
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PMessageSendFailed, 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");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PBroadcastFailed);
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");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PBroadcastFailed);
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;
if (!SimpleP2P.Instance.CanQueueMessages(targets, data.Length, out var failedTarget, out var preflightReason))
{
var preflightError = $"P2P broadcast preflight failed: target={failedTarget}, reason={preflightReason}";
LogSystem.LogError(preflightError);
var isQueueLimit = preflightReason.IndexOf("queue", StringComparison.OrdinalIgnoreCase) >= 0
|| preflightReason.IndexOf("Queued", StringComparison.OrdinalIgnoreCase) >= 0;
NetworkPlayerTipManager.Instance.Request(isQueueLimit
? NetworkPlayerTipType.P2PQueueFull
: NetworkPlayerTipType.P2PBroadcastFailed);
OnLobbyErrorEvent?.Invoke(preflightError);
return false;
}
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}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PBroadcastFailed);
return false;
}
return true;
}
var error = $"P2P broadcast failed for all targets: targets={targets.Count}, bytes={data.Length}";
LogSystem.LogError(error);
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.P2PBroadcastFailed);
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;
if (!TrySteamApi("EnumerateMembers.GetNumLobbyMembers", () => SteamMatchmaking.GetNumLobbyMembers(CurrentLobby), out var count)) yield break;
for (int i = 0; i < count; i++)
{
if (!TrySteamApi("EnumerateMembers.GetLobbyMemberByIndex", () => SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i), out var member)) yield break;
yield return member;
}
}
// 获取房间所有成员
public List<ulong> GetAllMemberIds()
{
return _memberInfos.Keys.ToList();
}
// 刷新房间内的成员信息
private void UpdateMemberInfo()
{
if (!CurrentLobby.IsValid()) return;
_memberInfos.Clear();
if (!TrySteamApi("UpdateMemberInfo.GetNumLobbyMembers", () => SteamMatchmaking.GetNumLobbyMembers(CurrentLobby), out var count)) return;
for (int i = 0; i < count; i++)
{
if (!TrySteamApi("UpdateMemberInfo.GetLobbyMemberByIndex", () => SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i), out var cSteamId)) return;
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;
if (!TrySteamApi("UpdateMemberInfo.GetFriendPersonaName", () => SteamFriends.GetFriendPersonaName(cSteamId), out var memberName)) return;
_memberInfos[cSteamId.m_SteamID].Name = memberName;
_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 TrySteamApi("GetMemberCount.GetNumLobbyMembers", () => SteamMatchmaking.GetNumLobbyMembers(CurrentLobby), out var count) ? count : 0;
}
// 获取最大成员数
public int GetMemberLimit()
{
if (!CurrentLobby.IsValid()) return 0;
return SteamMatchmaking.GetLobbyMemberLimit(CurrentLobby);
}
// 设置最大成员数
public bool SetMemberLimit(int maxMembers)
{
if (!IsLobbyOwner())
{
LogSystem.LogError("Only lobby owner can change lobby member limit");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (!CurrentLobby.IsValid())
{
LogSystem.LogError("Not in a lobby");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
if (maxMembers <= 0)
{
LogSystem.LogError($"Invalid lobby member limit: {maxMembers}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
int memberCount = GetMemberCount();
if (maxMembers < memberCount)
{
LogSystem.LogError($"Cannot set lobby member limit below current member count: limit={maxMembers}, current={memberCount}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
return false;
}
bool success = SteamMatchmaking.SetLobbyMemberLimit(CurrentLobby, maxMembers);
if (success)
{
LogSystem.LogInfo($"Lobby member limit changed to: {maxMembers}");
EventManager.Publish(new UpdateUIOutsideMultiplayLobbyList());
}
else
{
LogSystem.LogError($"Failed to change lobby member limit to: {maxMembers}");
NetworkPlayerTipManager.Instance.Request(NetworkPlayerTipType.LobbyOperationFailed);
}
return success;
}
// 判断成员是否在房间中
public bool IsMemberInLobby(ulong memberId)
{
if (!CurrentLobby.IsValid()) return false;
if (!TrySteamApi("IsMemberInLobby.GetNumLobbyMembers", () => SteamMatchmaking.GetNumLobbyMembers(CurrentLobby), out var count)) return false;
for (int i = 0; i < count; i++)
{
if (!TrySteamApi("IsMemberInLobby.GetLobbyMemberByIndex", () => SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i), out var cSteamId)) return false;
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()
{
if (_selfID.IsValid()) return _selfID.m_SteamID;
return TrySteamApi("GetSelfMemberId.GetSteamID", SteamUser.GetSteamID, out var selfId) ? selfId.m_SteamID : 0;
}
// 获取房主的 memberId
public ulong GetLobbyOwnerId()
{
if (!CurrentLobby.IsValid()) return 0;
return TrySteamApi("GetLobbyOwnerId.GetLobbyOwner", () => SteamMatchmaking.GetLobbyOwner(CurrentLobby), out var owner) ? owner.m_SteamID : 0;
}
// 获取当前房间状态
public LobbyState GetCurState()
{
return CurrentState;
}
// 自己是否是房主
public bool IsLobbyOwner()
{
if (!IsInLobby()) return false;
if (!TrySteamApi("IsLobbyOwner.GetLobbyOwner", () => SteamMatchmaking.GetLobbyOwner(CurrentLobby), out var owner)) return false;
if (!TrySteamApi("IsLobbyOwner.GetSteamID", SteamUser.GetSteamID, out var selfId)) return false;
return owner == selfId;
}
// 自己是否在房间中
public bool IsInLobby()
{
return !_steamApiUnavailable && 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();
_cbLobbyDataUpdate?.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;
public bool HasPassword;
public int ReportCount;
}
}