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
}
}