TH1/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs

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