TH1/Unity/Assets/Scripts/TH1_Logic/Steam/SteamLobbyManager.cs
2025-09-18 14:16:09 +08:00

578 lines
22 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.Collections.Generic;
using System.Linq;
using Logic.CrashSight;
using Steamworks;
using TH1_Logic.Net;
using UnityEngine;
namespace TH1_Logic.Steam
{
public class SteamLobbyManager : ILobby
{
public CSteamID CurrentLobby = CSteamID.Nil;
public CSteamID CachedOwner = CSteamID.Nil;
public LobbyState CurrentState { get; private set; } = LobbyState.None;
// 事件委托
public event System.Action<CSteamID> OnLobbyCreatedEvent; // 房间创建成功
public event System.Action<CSteamID> OnLobbyEnteredEvent; // 进入房间
public event System.Action 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 的本机(创建者)。
// 何时触发SteamMatchmaking.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;
// 初始化
public void InitCallbacks()
{
_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");
}
// 建房
public void CreateFriendsLobby(int maxMembers = 4)
{
if (CurrentState != LobbyState.None)
{
LogSystem.LogInfo($"Cannot create lobby in state: {CurrentState}");
return;
}
CurrentState = LobbyState.Creating;
LogSystem.LogInfo($"Creating friends lobby with max members: {maxMembers}");
SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypeFriendsOnly, maxMembers);
}
// 加入房间
public void JoinLobby(CSteamID lobbyId)
{
if (CurrentState != LobbyState.None)
{
LogSystem.LogInfo($"Cannot join lobby in state: {CurrentState}");
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();
}
// 解散房间(仅房主可用)
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(CSteamID memberToKick)
{
if (!IsLobbyOwner())
{
OnLobbyErrorEvent?.Invoke("Only lobby owner can kick members");
return;
}
if (memberToKick == SteamUser.GetSteamID())
{
OnLobbyErrorEvent?.Invoke("Cannot kick yourself");
return;
}
LogSystem.LogInfo($"Kicking member: {memberToKick}");
// 通过设置成员数据标记被踢,客户端检测到后自动离开
SteamMatchmaking.SetLobbyMemberData(CurrentLobby, $"kick_{memberToKick.m_SteamID}", "true");
}
// 邀请好友
public void InviteFriend(CSteamID friendId)
{
if (!CurrentLobby.IsValid())
{
OnLobbyErrorEvent?.Invoke("Not in a lobby");
return;
}
LogSystem.LogInfo($"Inviting friend: {friendId}");
SteamMatchmaking.InviteUserToLobby(CurrentLobby, friendId);
}
// 打开好友邀请对话框
public void OpenInviteOverlay()
{
if (!CurrentLobby.IsValid())
{
OnLobbyErrorEvent?.Invoke("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 SetLobbyData(string key, string value)
{
if (!IsLobbyOwner())
{
OnLobbyErrorEvent?.Invoke("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;
OnLobbyErrorEvent?.Invoke($"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, "owner", SteamUser.GetSteamID().m_SteamID.ToString());
SteamMatchmaking.SetLobbyData(CurrentLobby, "version", "1.0");
SteamMatchmaking.SetLobbyData(CurrentLobby, "game_mode", "default");
// 设置Rich Presence
SteamFriends.SetRichPresence("connect", "+connect_lobby " + CurrentLobby.m_SteamID);
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;
LogSystem.LogError($"Failed to enter lobby: {(EChatRoomEnterResponse)data.m_EChatRoomEnterResponse}");
return;
}
CurrentLobby = new CSteamID(data.m_ulSteamIDLobby);
CachedOwner = SteamMatchmaking.GetLobbyOwner(CurrentLobby);
CurrentState = LobbyState.InLobby;
LogSystem.LogInfo($"Successfully entered lobby: {CurrentLobby}");
// 检查是否被踢
CheckIfKicked();
OnLobbyReadyInternal();
OnLobbyMembersChangedInternal();
}
// 成员变化回调
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);
// 如果我是房主,尝试连接到新成员
if (IsLobbyOwner() && changedUser != SteamUser.GetSteamID())
{
SimpleP2P.Instance.ConnectToPeer(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");
// 新房主连接到所有其他成员
foreach (var member in EnumerateMembers())
{
if (member != SteamUser.GetSteamID())
{
SimpleP2P.Instance.ConnectToPeer(member);
}
}
}
else
{
LogSystem.LogInfo($"New host is: {CachedOwner}");
// 普通成员断开旧连接,连接新房主
SimpleP2P.Instance.DisconnectAll();
SimpleP2P.Instance.ConnectToPeer(CachedOwner);
}
}
// 房间准备完毕时内部处理
private void OnLobbyReadyInternal()
{
// 如果不是房主,连接到房主
if (!IsLobbyOwner() && CachedOwner.IsValid())
{
SimpleP2P.Instance.ConnectToPeer(CachedOwner);
}
}
// 有成员变化时内部处理
private void OnLobbyMembersChangedInternal()
{
var members = EnumerateMembers().ToList();
LogSystem.LogInfo($"Lobby members changed. Count: {members.Count}");
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)
{
LogSystem.LogInfo($"P2P connection established with: {steamID}");
}
private void OnP2PPeerDisconnected(CSteamID steamID)
{
LogSystem.LogInfo($"P2P connection lost with: {steamID}");
}
private void OnP2PConnectionError(string error)
{
LogSystem.LogError($"P2P connection error: {error}");
}
// 轮询P2P消息需要在Update中调用
public void Update()
{
SimpleP2P.Instance.PollMessages();
}
// 发送P2P消息
public bool SendMessageToPeer(ulong member, byte[] data, bool reliable = true)
{
if (!IsInLobby())
{
OnLobbyErrorEvent?.Invoke("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())
{
OnLobbyErrorEvent?.Invoke("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()
{
if (!CurrentLobby.IsValid()) return new List<ulong>();
int count = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
var list = new List<ulong>();
for (int i = 0; i < count; i++)
{
var cSteamId = SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
list.Add(cSteamId.m_SteamID);
}
return list;
}
// 获取成员数量
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 (!CurrentLobby.IsValid()) return CSteamID.Nil;
int count = SteamMatchmaking.GetNumLobbyMembers(CurrentLobby);
for (int i = 0; i < count; i++)
{
var cSteamId = SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
if (cSteamId.m_SteamID == memberId) return cSteamId;
}
return CSteamID.Nil;
}
// 获取自己的 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 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");
}
}
}