#if STEAMWORKSNET using System; using System.Collections; using System.Collections.Generic; using Steamworks; using UnityEngine; namespace MegaKoop.Steam { /// /// Steam Lobby Service using Steamworks.NET /// Handles create/join/leave/invite and lobby member updates. /// 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 OnJoinFailed; // reason public event Action OnAvatarUpdated; // steamId string private Callback _cbLobbyCreated; private Callback _cbLobbyEnter; private Callback _cbLobbyChatUpdate; private Callback _cbLobbyDataUpdate; private Callback _cbLobbyMatchList; private Callback _cbLobbyJoinRequested; private Callback _cbRichPresenceJoinRequested; private Callback _cbOverlayActivated; private Callback _cbAvatarImageLoaded; private CSteamID _lobbyId; private bool _inLobby; private readonly List _members = new(); private readonly System.Collections.Generic.Dictionary _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.Create(OnLobbyCreatedCb); _cbLobbyEnter = Callback.Create(OnLobbyEnterCb); _cbLobbyChatUpdate = Callback.Create(OnLobbyChatUpdateCb); _cbLobbyDataUpdate = Callback.Create(OnLobbyDataUpdateCb); _cbLobbyMatchList = Callback.Create(OnLobbyMatchListCb); _cbLobbyJoinRequested = Callback.Create(OnLobbyJoinRequestedCb); _cbRichPresenceJoinRequested = Callback.Create(OnRichPresenceJoinRequestedCb); _cbOverlayActivated = Callback.Create(OnOverlayActivatedCb); _cbAvatarImageLoaded = Callback.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) { // Parse steam://joinlobby/// and join the lobby string conn = cb.m_rgchConnect; if (!string.IsNullOrEmpty(conn)) { try { var s = conn.Trim(); const string prefix = "steam://joinlobby/"; if (s.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { var rest = s.Substring(prefix.Length); var parts = rest.Split('/'); // Expected: [appId, lobbyId, steamId] if (parts.Length >= 2) { if (ulong.TryParse(parts[1], out var lobbyId)) { var lobby = new CSteamID(lobbyId); SteamMatchmaking.JoinLobby(lobby); return; } } } } catch (Exception ex) { Debug.LogWarning($"[SteamLobbyService] Failed to parse rich presence connect '{conn}': {ex.Message}"); } } Debug.LogWarning($"[SteamLobbyService] RichPresence join requested but could not parse connect '{conn}'."); } #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 OnJoinFailed; public event Action 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