470 lines
20 KiB
C#
470 lines
20 KiB
C#
#if STEAMWORKSNET
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Steamworks;
|
|
using UnityEngine;
|
|
|
|
namespace MegaKoop.Steam
|
|
{
|
|
/// <summary>
|
|
/// Steam Lobby Service using Steamworks.NET
|
|
/// Handles create/join/leave/invite and lobby member updates.
|
|
/// </summary>
|
|
public class SteamLobbyService : MonoBehaviour
|
|
{
|
|
public static SteamLobbyService Instance { get; private set; }
|
|
|
|
public bool IsInLobby => _lobbyId.m_SteamID != 0 && _inLobby;
|
|
public bool IsHost { get; private set; }
|
|
public string LobbyCode { get; private set; } = string.Empty;
|
|
public int MaxMembers { get; private set; } = 4;
|
|
public CSteamID LobbyId => _lobbyId;
|
|
public string LobbyIdString => _lobbyId.m_SteamID.ToString();
|
|
public string LocalSteamIdString => SteamManager.Initialized ? SteamUser.GetSteamID().m_SteamID.ToString() : "0";
|
|
public string HostSteamIdString => IsInLobby ? SteamMatchmaking.GetLobbyData(_lobbyId, "host") : string.Empty;
|
|
|
|
public event Action OnLobbyCreated;
|
|
public event Action OnLobbyEntered;
|
|
public event Action OnLobbyLeft;
|
|
public event Action OnMembersChanged;
|
|
public event Action OnLobbyDataUpdated;
|
|
public event Action<string> OnJoinFailed; // reason
|
|
public event Action<string> OnAvatarUpdated; // steamId string
|
|
|
|
private Callback<LobbyCreated_t> _cbLobbyCreated;
|
|
private Callback<LobbyEnter_t> _cbLobbyEnter;
|
|
private Callback<LobbyChatUpdate_t> _cbLobbyChatUpdate;
|
|
private Callback<LobbyDataUpdate_t> _cbLobbyDataUpdate;
|
|
private Callback<LobbyMatchList_t> _cbLobbyMatchList;
|
|
private Callback<GameLobbyJoinRequested_t> _cbLobbyJoinRequested;
|
|
private Callback<GameRichPresenceJoinRequested_t> _cbRichPresenceJoinRequested;
|
|
private Callback<GameOverlayActivated_t> _cbOverlayActivated;
|
|
private Callback<AvatarImageLoaded_t> _cbAvatarImageLoaded;
|
|
|
|
private CSteamID _lobbyId;
|
|
private bool _inLobby;
|
|
|
|
private readonly List<CSteamID> _members = new();
|
|
private readonly System.Collections.Generic.Dictionary<ulong, Sprite> _avatarCache = new();
|
|
private bool _overlayWarned = false;
|
|
private bool _lastOverlayActive = false;
|
|
|
|
private void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
if (!SteamManager.Initialized)
|
|
{
|
|
Debug.LogWarning("[SteamLobbyService] Steam not initialized. Service is idle.");
|
|
return;
|
|
}
|
|
|
|
_cbLobbyCreated = Callback<LobbyCreated_t>.Create(OnLobbyCreatedCb);
|
|
_cbLobbyEnter = Callback<LobbyEnter_t>.Create(OnLobbyEnterCb);
|
|
_cbLobbyChatUpdate = Callback<LobbyChatUpdate_t>.Create(OnLobbyChatUpdateCb);
|
|
_cbLobbyDataUpdate = Callback<LobbyDataUpdate_t>.Create(OnLobbyDataUpdateCb);
|
|
_cbLobbyMatchList = Callback<LobbyMatchList_t>.Create(OnLobbyMatchListCb);
|
|
_cbLobbyJoinRequested = Callback<GameLobbyJoinRequested_t>.Create(OnLobbyJoinRequestedCb);
|
|
_cbRichPresenceJoinRequested = Callback<GameRichPresenceJoinRequested_t>.Create(OnRichPresenceJoinRequestedCb);
|
|
_cbOverlayActivated = Callback<GameOverlayActivated_t>.Create(OnOverlayActivatedCb);
|
|
_cbAvatarImageLoaded = Callback<AvatarImageLoaded_t>.Create(OnAvatarImageLoadedCb);
|
|
}
|
|
|
|
#region Public API
|
|
public void CreateLobby(int maxPlayers, bool isPublic)
|
|
{
|
|
if (!SteamManager.Initialized) { OnJoinFailed?.Invoke("Steam not initialized"); return; }
|
|
MaxMembers = maxPlayers;
|
|
ELobbyType type = isPublic ? ELobbyType.k_ELobbyTypePublic : ELobbyType.k_ELobbyTypeFriendsOnly;
|
|
SteamMatchmaking.CreateLobby(type, maxPlayers);
|
|
}
|
|
|
|
public void LeaveLobby()
|
|
{
|
|
if (!IsInLobby) return;
|
|
SteamMatchmaking.LeaveLobby(_lobbyId);
|
|
_inLobby = false;
|
|
_members.Clear();
|
|
_lobbyId = default;
|
|
IsHost = false;
|
|
LobbyCode = string.Empty;
|
|
OnLobbyLeft?.Invoke();
|
|
}
|
|
|
|
public void InviteFriends()
|
|
{
|
|
if (!IsInLobby) return;
|
|
_lastOverlayActive = false;
|
|
SteamFriends.ActivateGameOverlayInviteDialog(_lobbyId);
|
|
StartCoroutine(EnsureOverlayOpenedRoutine());
|
|
}
|
|
|
|
public void OpenFriendsOverlay()
|
|
{
|
|
SteamFriends.ActivateGameOverlay("Friends");
|
|
}
|
|
|
|
private void OnOverlayActivatedCb(GameOverlayActivated_t cb)
|
|
{
|
|
_lastOverlayActive = cb.m_bActive != 0;
|
|
}
|
|
|
|
private IEnumerator EnsureOverlayOpenedRoutine()
|
|
{
|
|
float t = 0f;
|
|
const float timeout = 0.5f;
|
|
while (t < timeout)
|
|
{
|
|
if (_lastOverlayActive) yield break;
|
|
t += Time.unscaledDeltaTime;
|
|
yield return null;
|
|
}
|
|
if (!_lastOverlayActive)
|
|
{
|
|
if (!_overlayWarned)
|
|
{
|
|
Debug.LogWarning("[SteamLobbyService] Overlay did not activate; opening Friends overlay as fallback.");
|
|
_overlayWarned = true;
|
|
}
|
|
SteamFriends.ActivateGameOverlay("Friends");
|
|
}
|
|
}
|
|
|
|
public void Kick(string steamId)
|
|
{
|
|
if (!IsInLobby || !IsHost) return;
|
|
if (ulong.TryParse(steamId, out ulong sid))
|
|
{
|
|
// KickLobbyMember is not exposed in some Steamworks.NET versions.
|
|
// Try via reflection, otherwise log a warning and rely on custom policy (client leaves on request).
|
|
try
|
|
{
|
|
var mm = typeof(SteamMatchmaking);
|
|
var mi = mm.GetMethod("KickLobbyMember", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
|
if (mi != null)
|
|
{
|
|
mi.Invoke(null, new object[] { _lobbyId, new CSteamID(sid) });
|
|
return;
|
|
}
|
|
}
|
|
catch { }
|
|
// Fallback: set a lobby data key to signal the target client to leave by itself
|
|
var key = $"kick_{sid}";
|
|
SteamMatchmaking.SetLobbyData(_lobbyId, key, "1");
|
|
Debug.LogWarning($"[SteamLobbyService] KickLobbyMember not available; set lobby key '{key}' to signal client to leave.");
|
|
}
|
|
}
|
|
|
|
public void SetReady(bool ready)
|
|
{
|
|
if (!IsInLobby) return;
|
|
SteamMatchmaking.SetLobbyMemberData(_lobbyId, "ready", ready ? "1" : "0");
|
|
}
|
|
|
|
public void StartGameSignal()
|
|
{
|
|
if (!IsInLobby || !IsHost) return;
|
|
SteamMatchmaking.SetLobbyData(_lobbyId, "start", "1");
|
|
}
|
|
|
|
public bool IsStartSignaled()
|
|
{
|
|
if (!IsInLobby) return false;
|
|
return SteamMatchmaking.GetLobbyData(_lobbyId, "start") == "1";
|
|
}
|
|
|
|
public bool IsOverlayEnabled() => SteamUtils.IsOverlayEnabled();
|
|
|
|
public bool TryGetAvatarSprite(string steamId, out Sprite sprite, bool large = true)
|
|
{
|
|
sprite = null;
|
|
if (!ulong.TryParse(steamId, out var sid)) return false;
|
|
if (_avatarCache.TryGetValue(sid, out var cached)) { sprite = cached; return true; }
|
|
var csid = new CSteamID(sid);
|
|
int imageId = large ? SteamFriends.GetLargeFriendAvatar(csid) : SteamFriends.GetSmallFriendAvatar(csid);
|
|
if (imageId <= 0)
|
|
{
|
|
// Ask Steam to fetch persona data (which includes avatar) and try later via callback
|
|
SteamFriends.RequestUserInformation(csid, true);
|
|
return false;
|
|
}
|
|
if (!SteamUtils.GetImageSize(imageId, out uint w, out uint h) || w == 0 || h == 0) return false;
|
|
byte[] data = new byte[w * h * 4];
|
|
if (!SteamUtils.GetImageRGBA(imageId, data, (int)data.Length)) return false;
|
|
// Flip vertically (Steam image origin differs from Unity)
|
|
var flipped = new byte[data.Length];
|
|
int rowSize = (int)w * 4;
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
int src = (int)((h - 1 - y) * rowSize);
|
|
int dst = (int)(y * rowSize);
|
|
System.Buffer.BlockCopy(data, src, flipped, dst, rowSize);
|
|
}
|
|
var tex = new Texture2D((int)w, (int)h, TextureFormat.RGBA32, false);
|
|
tex.LoadRawTextureData(flipped);
|
|
tex.Apply();
|
|
var spr = Sprite.Create(tex, new Rect(0,0, tex.width, tex.height), new Vector2(0.5f,0.5f), 100f);
|
|
_avatarCache[sid] = spr;
|
|
sprite = spr;
|
|
return true;
|
|
}
|
|
|
|
private void OnAvatarImageLoadedCb(AvatarImageLoaded_t cb)
|
|
{
|
|
// Build and cache sprite, then notify listeners
|
|
if (!SteamUtils.GetImageSize(cb.m_iImage, out uint w, out uint h) || w == 0 || h == 0) return;
|
|
byte[] data = new byte[w * h * 4];
|
|
if (!SteamUtils.GetImageRGBA(cb.m_iImage, data, (int)data.Length)) return;
|
|
// Flip vertically
|
|
var flipped = new byte[data.Length];
|
|
int rowSize = (int)w * 4;
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
int src = (int)((h - 1 - y) * rowSize);
|
|
int dst = (int)(y * rowSize);
|
|
System.Buffer.BlockCopy(data, src, flipped, dst, rowSize);
|
|
}
|
|
var tex = new Texture2D((int)w, (int)h, TextureFormat.RGBA32, false);
|
|
tex.LoadRawTextureData(flipped);
|
|
tex.Apply();
|
|
var spr = Sprite.Create(tex, new Rect(0,0, tex.width, tex.height), new Vector2(0.5f,0.5f), 100f);
|
|
var sid = cb.m_steamID.m_SteamID;
|
|
_avatarCache[sid] = spr;
|
|
OnAvatarUpdated?.Invoke(sid.ToString());
|
|
}
|
|
|
|
public IReadOnlyList<(string steamId, string name, bool online)> GetFriends()
|
|
{
|
|
var list = new List<(string, string, bool)>();
|
|
if (!SteamManager.Initialized) return list;
|
|
int count = SteamFriends.GetFriendCount(EFriendFlags.k_EFriendFlagImmediate);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var fid = SteamFriends.GetFriendByIndex(i, EFriendFlags.k_EFriendFlagImmediate);
|
|
var name = SteamFriends.GetFriendPersonaName(fid);
|
|
var state = SteamFriends.GetFriendPersonaState(fid);
|
|
bool online = state != EPersonaState.k_EPersonaStateOffline;
|
|
// Proactively request avatar if not ready yet
|
|
int imgId = SteamFriends.GetSmallFriendAvatar(fid);
|
|
if (imgId <= 0) SteamFriends.RequestUserInformation(fid, true);
|
|
list.Add((fid.m_SteamID.ToString(), name, online));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private string GetLobbyConnectString()
|
|
{
|
|
if (!IsInLobby) return string.Empty;
|
|
var appId = SteamUtils.GetAppID().m_AppId.ToString();
|
|
return $"steam://joinlobby/{appId}/{_lobbyId.m_SteamID}/{SteamUser.GetSteamID().m_SteamID}";
|
|
}
|
|
|
|
public void InviteFriendBySteamId(string steamId)
|
|
{
|
|
if (!IsInLobby) return;
|
|
if (!ulong.TryParse(steamId, out var fid)) return;
|
|
var conn = GetLobbyConnectString();
|
|
if (string.IsNullOrEmpty(conn)) return;
|
|
SteamFriends.SetRichPresence("connect", conn);
|
|
SteamFriends.InviteUserToGame(new CSteamID(fid), conn);
|
|
Debug.Log($"[SteamLobbyService] Sent game invite to {steamId} using connect string.");
|
|
}
|
|
|
|
public bool IsKickedLocal()
|
|
{
|
|
if (!IsInLobby) return false;
|
|
var key = $"kick_{LocalSteamIdString}";
|
|
return SteamMatchmaking.GetLobbyData(_lobbyId, key) == "1";
|
|
}
|
|
|
|
public IReadOnlyList<(string steamId, string name, bool isReady, bool isHost)> GetMembers()
|
|
{
|
|
var list = new List<(string, string, bool, bool)>();
|
|
if (!IsInLobby) return list;
|
|
int count = SteamMatchmaking.GetNumLobbyMembers(_lobbyId);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var member = SteamMatchmaking.GetLobbyMemberByIndex(_lobbyId, i);
|
|
bool ready = SteamMatchmaking.GetLobbyMemberData(_lobbyId, member, "ready") == "1";
|
|
bool isHost = SteamMatchmaking.GetLobbyOwner(_lobbyId) == member;
|
|
string name = SteamFriends.GetFriendPersonaName(member);
|
|
list.Add((member.m_SteamID.ToString(), name, ready, isHost));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public void JoinLobbyByCode(string code)
|
|
{
|
|
if (!SteamManager.Initialized) { OnJoinFailed?.Invoke("Steam not initialized"); return; }
|
|
SteamMatchmaking.AddRequestLobbyListStringFilter("code", code, ELobbyComparison.k_ELobbyComparisonEqual);
|
|
SteamMatchmaking.RequestLobbyList();
|
|
}
|
|
#endregion
|
|
|
|
#region Callbacks
|
|
private void OnLobbyCreatedCb(LobbyCreated_t cb)
|
|
{
|
|
if (cb.m_eResult != EResult.k_EResultOK)
|
|
{
|
|
OnJoinFailed?.Invoke($"Create failed: {cb.m_eResult}");
|
|
return;
|
|
}
|
|
_lobbyId = new CSteamID(cb.m_ulSteamIDLobby);
|
|
IsHost = true;
|
|
_inLobby = true;
|
|
LobbyCode = GenerateCode();
|
|
|
|
SteamMatchmaking.SetLobbyData(_lobbyId, "code", LobbyCode);
|
|
SteamMatchmaking.SetLobbyData(_lobbyId, "name", SteamFriends.GetPersonaName() + "'s Lobby");
|
|
SteamMatchmaking.SetLobbyData(_lobbyId, "host", SteamUser.GetSteamID().m_SteamID.ToString());
|
|
SteamMatchmaking.SetLobbyMemberLimit(_lobbyId, MaxMembers);
|
|
SteamMatchmaking.SetLobbyJoinable(_lobbyId, true);
|
|
SteamMatchmaking.SetLobbyMemberData(_lobbyId, "ready", "1");
|
|
|
|
RefreshMembers();
|
|
OnLobbyCreated?.Invoke();
|
|
OnLobbyEntered?.Invoke();
|
|
OnLobbyDataUpdated?.Invoke();
|
|
}
|
|
|
|
private void OnLobbyEnterCb(LobbyEnter_t cb)
|
|
{
|
|
_lobbyId = new CSteamID(cb.m_ulSteamIDLobby);
|
|
_inLobby = true;
|
|
IsHost = SteamMatchmaking.GetLobbyOwner(_lobbyId) == SteamUser.GetSteamID();
|
|
LobbyCode = SteamMatchmaking.GetLobbyData(_lobbyId, "code");
|
|
if (!IsHost)
|
|
SteamMatchmaking.SetLobbyMemberData(_lobbyId, "ready", "0");
|
|
RefreshMembers();
|
|
OnLobbyEntered?.Invoke();
|
|
}
|
|
|
|
private void OnLobbyChatUpdateCb(LobbyChatUpdate_t cb)
|
|
{
|
|
if (new CSteamID(cb.m_ulSteamIDLobby) != _lobbyId) return;
|
|
RefreshMembers();
|
|
OnMembersChanged?.Invoke();
|
|
}
|
|
|
|
private void OnLobbyDataUpdateCb(LobbyDataUpdate_t cb)
|
|
{
|
|
if (new CSteamID(cb.m_ulSteamIDLobby) != _lobbyId) return;
|
|
OnLobbyDataUpdated?.Invoke();
|
|
}
|
|
|
|
private void OnLobbyMatchListCb(LobbyMatchList_t cb)
|
|
{
|
|
int lobbies = (int)cb.m_nLobbiesMatching;
|
|
if (lobbies <= 0)
|
|
{
|
|
OnJoinFailed?.Invoke("No lobby with this code");
|
|
return;
|
|
}
|
|
var id = SteamMatchmaking.GetLobbyByIndex(0);
|
|
SteamMatchmaking.JoinLobby(id);
|
|
}
|
|
|
|
private void OnLobbyJoinRequestedCb(GameLobbyJoinRequested_t cb)
|
|
{
|
|
SteamMatchmaking.JoinLobby(cb.m_steamIDLobby);
|
|
}
|
|
|
|
private void OnRichPresenceJoinRequestedCb(GameRichPresenceJoinRequested_t cb)
|
|
{
|
|
// Some Steamworks.NET versions expose only m_rgchConnect here.
|
|
// We cannot reliably parse lobby id across versions; log and rely on GameLobbyJoinRequested_t for lobby joins.
|
|
Debug.Log("[SteamLobbyService] RichPresence join requested - handle via GameLobbyJoinRequested or custom connect string.");
|
|
}
|
|
#endregion
|
|
|
|
private void RefreshMembers()
|
|
{
|
|
_members.Clear();
|
|
if (!IsInLobby) return;
|
|
int count = SteamMatchmaking.GetNumLobbyMembers(_lobbyId);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
_members.Add(SteamMatchmaking.GetLobbyMemberByIndex(_lobbyId, i));
|
|
}
|
|
}
|
|
|
|
private static string GenerateCode()
|
|
{
|
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
System.Text.StringBuilder sb = new System.Text.StringBuilder(6);
|
|
var rnd = new System.Random();
|
|
for (int i = 0; i < 6; i++) sb.Append(chars[rnd.Next(chars.Length)]);
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace MegaKoop.Steam
|
|
{
|
|
// Safe stub without Steamworks.NET
|
|
public class SteamLobbyService : MonoBehaviour
|
|
{
|
|
public static SteamLobbyService Instance { get; private set; }
|
|
public bool IsInLobby => _isInLobby;
|
|
public bool IsHost { get; private set; }
|
|
public string LobbyCode { get; private set; } = string.Empty;
|
|
public int MaxMembers { get; private set; } = 4;
|
|
public string LobbyIdString => "0";
|
|
public string LocalSteamIdString => "0";
|
|
|
|
public event Action OnLobbyCreated;
|
|
public event Action OnLobbyEntered;
|
|
public event Action OnLobbyLeft;
|
|
public event Action OnMembersChanged;
|
|
public event Action OnLobbyDataUpdated;
|
|
public event Action<string> OnJoinFailed;
|
|
public event Action<string> OnAvatarUpdated;
|
|
|
|
private bool _isInLobby;
|
|
|
|
private void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
Debug.LogWarning("[SteamLobbyService] Stub active. Install Steamworks.NET and define STEAMWORKSNET.");
|
|
}
|
|
|
|
public void CreateLobby(int maxPlayers, bool isPublic)
|
|
{
|
|
MaxMembers = maxPlayers; IsHost = true; _isInLobby = true; LobbyCode = "AAAAAA";
|
|
OnLobbyCreated?.Invoke(); OnLobbyEntered?.Invoke(); OnLobbyDataUpdated?.Invoke();
|
|
}
|
|
public void LeaveLobby() { _isInLobby = false; IsHost = false; LobbyCode = string.Empty; OnLobbyLeft?.Invoke(); }
|
|
public void InviteFriends() { Debug.Log("[SteamLobbyService] InviteFriends stub"); }
|
|
public void Kick(string steamId) { Debug.Log($"[SteamLobbyService] Kick stub {steamId}"); }
|
|
public void SetReady(bool ready) { Debug.Log($"[SteamLobbyService] SetReady {ready}"); OnLobbyDataUpdated?.Invoke(); }
|
|
public void StartGameSignal() { Debug.Log("[SteamLobbyService] StartGame signal"); }
|
|
public bool IsStartSignaled() => false;
|
|
public bool IsOverlayEnabled() => false;
|
|
public bool TryGetAvatarSprite(string steamId, out Sprite sprite, bool large = true) { sprite = null; return false; }
|
|
public void OpenFriendsOverlay() { Debug.Log("[SteamLobbyService] OpenFriendsOverlay stub"); }
|
|
public IReadOnlyList<(string steamId, string name, bool online)> GetFriends() => new List<(string, string, bool)>();
|
|
public void InviteFriendBySteamId(string steamId) { Debug.Log("[SteamLobbyService] InviteFriendBySteamId stub"); }
|
|
public bool IsKickedLocal() => false;
|
|
public IReadOnlyList<(string steamId, string name, bool isReady, bool isHost)> GetMembers() => new List<(string,string,bool,bool)>();
|
|
public void JoinLobbyByCode(string code) { _isInLobby = true; IsHost = false; LobbyCode = code; OnLobbyEntered?.Invoke(); }
|
|
}
|
|
}
|
|
#endif
|