2148 lines
91 KiB
C#
2148 lines
91 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.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 = 目标 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<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;
|
||
}
|
||
}
|