using System; using System.Collections.Generic; using Steamworks; using UnityEngine; using Ashwild.Player; namespace Ashwild.Network { /// /// The one place that bridges Steam friend invites into the game. It does three things: /// • SEND — opens the Steam overlay friends list so the local player can invite a friend to /// their current session (the in-game pause menu "Invite" button calls OpenInviteOverlay). /// • PRESENCE — publishes the session's connect code as Steam Rich Presence while online, so /// friends see "Join Game" and invites carry the code that JoinSession understands. /// • RECEIVE — listens for Steam's GameRichPresenceJoinRequested callback (fired when a friend /// accepts our invite or clicks "Join Game") and raises PlayerEvents.InviteReceived so the /// menu can pop the invitation card. Also resolves friend avatars (async via Steam). /// Place this on the persistent FishNet NetworkManager GameObject, beside NetworkSessionManager. /// [DisallowMultipleComponent] public class SteamInviteService : MonoBehaviour { #region Constants /// /// Steam Rich Presence key that makes the "Join Game" entry appear for friends. /// private const string ConnectKey = "connect"; #endregion #region State /// /// Global access point used by the invite button and the invite popup. /// public static SteamInviteService Instance { get; private set; } /// /// Steam callback fired when a friend accepts our invite or clicks "Join Game". /// private Callback joinRequestedCallback; /// /// Steam callback fired when an avatar image finishes downloading from Steam's servers. /// private Callback avatarLoadedCallback; /// /// Avatar requests still waiting on Steam to finish downloading the image, by SteamID64. /// private readonly Dictionary> pendingAvatars = new Dictionary>(); /// /// True once the Steam callbacks have been registered (guards the deferred init). /// private bool callbacksReady; #endregion #region Unity Lifecycle /// /// Establishes the singleton; the GameObject persists via the NetworkManager itself. /// private void Awake() { if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; } /// /// Subscribes to the session bus and registers the Steam callbacks (once Steam is up). /// private void OnEnable() { PlayerEvents.SessionStarted += HandleSessionOnline; PlayerEvents.SessionJoined += HandleSessionJoined; PlayerEvents.SessionStopped += HandleSessionStopped; EnsureCallbacks(); } /// /// Unsubscribes from the bus and disposes the Steam callbacks — mirrors OnEnable. /// private void OnDisable() { PlayerEvents.SessionStarted -= HandleSessionOnline; PlayerEvents.SessionJoined -= HandleSessionJoined; PlayerEvents.SessionStopped -= HandleSessionStopped; joinRequestedCallback?.Dispose(); avatarLoadedCallback?.Dispose(); joinRequestedCallback = null; avatarLoadedCallback = null; callbacksReady = false; pendingAvatars.Clear(); } /// /// Retries callback registration until Steam is initialized, then stops checking. /// private void Update() { if (!callbacksReady) EnsureCallbacks(); } /// /// Clears the singleton reference. /// private void OnDestroy() { if (Instance == this) Instance = null; } #endregion #region Public API /// /// Opens the Steam overlay friends list so the player can invite a friend to the current /// session. Requires being in a session (host or client) so there is a code to share. /// public void OpenInviteOverlay() { if (!SteamManager.Initialized) { Debug.LogError("[SteamInvite] Steam is not initialized — cannot open the invite overlay.", this); return; } string code = PlayerEvents.SessionCode; if (string.IsNullOrEmpty(code)) { Debug.LogWarning("[SteamInvite] No active session — nothing to invite a friend to yet.", this); return; } SteamFriends.ActivateGameOverlayInviteDialogConnectString(code); } /// /// Resolves a friend's avatar as a Sprite. Returns it immediately via the callback when /// Steam already has it cached, otherwise waits for Steam to download it (may never fire /// if the friend has no avatar — callers should keep their fallback in that case). /// public void RequestAvatar(ulong steamId, Action onReady) { if (onReady == null) return; if (!SteamManager.Initialized) { onReady(null); return; } CSteamID id = new CSteamID(steamId); int handle = SteamFriends.GetLargeFriendAvatar(id); if (handle > 0) { onReady(SteamAvatarUtil.BuildSprite(handle)); return; } // -1 means Steam is fetching it from its servers → wait for AvatarImageLoaded_t. // 0 means the friend simply has no avatar set. if (handle == -1) pendingAvatars[steamId] = onReady; else onReady(null); } #endregion #region Steam Callbacks /// /// A friend accepted our invite / clicked "Join Game" — surface it to the menu popup. /// private void OnJoinRequested(GameRichPresenceJoinRequested_t cb) { string connect = cb.m_rgchConnect; if (string.IsNullOrEmpty(connect)) { Debug.LogWarning("[SteamInvite] Join request had an empty connect string — ignoring.", this); return; } ulong inviterId = cb.m_steamIDFriend.m_SteamID; string inviterName = cb.m_steamIDFriend.IsValid() ? SteamFriends.GetFriendPersonaName(cb.m_steamIDFriend) : "Un ami"; Debug.Log($"[SteamInvite] Invitation reçue de {inviterName} (connect={connect}).", this); PlayerEvents.RaiseInviteReceived(inviterId, inviterName, connect); } /// /// A requested avatar finished downloading — complete the matching pending request. /// private void OnAvatarLoaded(AvatarImageLoaded_t cb) { ulong id = cb.m_steamID.m_SteamID; if (!pendingAvatars.TryGetValue(id, out Action onReady)) return; pendingAvatars.Remove(id); onReady(SteamAvatarUtil.BuildSprite(cb.m_iImage)); } #endregion #region Event Handlers /// /// Host came online — advertise the session code so friends can join/invite. /// private void HandleSessionOnline(string code) => PublishConnect(code); /// /// Joined a host — advertise the same code so this client can also invite friends. /// private void HandleSessionJoined() => PublishConnect(PlayerEvents.SessionCode); /// /// Session ended — remove the Rich Presence so we stop advertising as joinable. /// private void HandleSessionStopped() { if (SteamManager.Initialized) SteamFriends.SetRichPresence(ConnectKey, null); } #endregion #region Internal Helpers /// /// Registers the Steam callbacks once Steam is initialized; safe to call repeatedly. /// private void EnsureCallbacks() { if (callbacksReady || !SteamManager.Initialized) return; joinRequestedCallback = Callback.Create(OnJoinRequested); avatarLoadedCallback = Callback.Create(OnAvatarLoaded); callbacksReady = true; } /// /// Writes the connect code into Steam Rich Presence (no-op when Steam/code is missing). /// private void PublishConnect(string code) { if (!SteamManager.Initialized || string.IsNullOrEmpty(code)) return; SteamFriends.SetRichPresence(ConnectKey, code); } #endregion } }