267 lines
9.3 KiB
C#
267 lines
9.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Steamworks;
|
|
using UnityEngine;
|
|
using Ashwild.Player;
|
|
|
|
namespace Ashwild.Network
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class SteamInviteService : MonoBehaviour
|
|
{
|
|
#region Constants
|
|
|
|
/// <summary>
|
|
/// Steam Rich Presence key that makes the "Join Game" entry appear for friends.
|
|
/// </summary>
|
|
private const string ConnectKey = "connect";
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// Global access point used by the invite button and the invite popup.
|
|
/// </summary>
|
|
public static SteamInviteService Instance { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Steam callback fired when a friend accepts our invite or clicks "Join Game".
|
|
/// </summary>
|
|
private Callback<GameRichPresenceJoinRequested_t> joinRequestedCallback;
|
|
|
|
/// <summary>
|
|
/// Steam callback fired when an avatar image finishes downloading from Steam's servers.
|
|
/// </summary>
|
|
private Callback<AvatarImageLoaded_t> avatarLoadedCallback;
|
|
|
|
/// <summary>
|
|
/// Avatar requests still waiting on Steam to finish downloading the image, by SteamID64.
|
|
/// </summary>
|
|
private readonly Dictionary<ulong, Action<Sprite>> pendingAvatars = new Dictionary<ulong, Action<Sprite>>();
|
|
|
|
/// <summary>
|
|
/// True once the Steam callbacks have been registered (guards the deferred init).
|
|
/// </summary>
|
|
private bool callbacksReady;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Establishes the singleton; the GameObject persists via the NetworkManager itself.
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(this);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to the session bus and registers the Steam callbacks (once Steam is up).
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
PlayerEvents.SessionStarted += HandleSessionOnline;
|
|
PlayerEvents.SessionJoined += HandleSessionJoined;
|
|
PlayerEvents.SessionStopped += HandleSessionStopped;
|
|
|
|
EnsureCallbacks();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes from the bus and disposes the Steam callbacks — mirrors OnEnable.
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.SessionStarted -= HandleSessionOnline;
|
|
PlayerEvents.SessionJoined -= HandleSessionJoined;
|
|
PlayerEvents.SessionStopped -= HandleSessionStopped;
|
|
|
|
joinRequestedCallback?.Dispose();
|
|
avatarLoadedCallback?.Dispose();
|
|
joinRequestedCallback = null;
|
|
avatarLoadedCallback = null;
|
|
callbacksReady = false;
|
|
|
|
pendingAvatars.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retries callback registration until Steam is initialized, then stops checking.
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
if (!callbacksReady) EnsureCallbacks();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the singleton reference.
|
|
/// </summary>
|
|
private void OnDestroy()
|
|
{
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public void RequestAvatar(ulong steamId, Action<Sprite> 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
|
|
|
|
/// <summary>
|
|
/// A friend accepted our invite / clicked "Join Game" — surface it to the menu popup.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A requested avatar finished downloading — complete the matching pending request.
|
|
/// </summary>
|
|
private void OnAvatarLoaded(AvatarImageLoaded_t cb)
|
|
{
|
|
ulong id = cb.m_steamID.m_SteamID;
|
|
if (!pendingAvatars.TryGetValue(id, out Action<Sprite> onReady)) return;
|
|
|
|
pendingAvatars.Remove(id);
|
|
onReady(SteamAvatarUtil.BuildSprite(cb.m_iImage));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
/// <summary>
|
|
/// Host came online — advertise the session code so friends can join/invite.
|
|
/// </summary>
|
|
private void HandleSessionOnline(string code) => PublishConnect(code);
|
|
|
|
/// <summary>
|
|
/// Joined a host — advertise the same code so this client can also invite friends.
|
|
/// </summary>
|
|
private void HandleSessionJoined() => PublishConnect(PlayerEvents.SessionCode);
|
|
|
|
/// <summary>
|
|
/// Session ended — remove the Rich Presence so we stop advertising as joinable.
|
|
/// </summary>
|
|
private void HandleSessionStopped()
|
|
{
|
|
if (SteamManager.Initialized)
|
|
SteamFriends.SetRichPresence(ConnectKey, null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Helpers
|
|
|
|
/// <summary>
|
|
/// Registers the Steam callbacks once Steam is initialized; safe to call repeatedly.
|
|
/// </summary>
|
|
private void EnsureCallbacks()
|
|
{
|
|
if (callbacksReady || !SteamManager.Initialized) return;
|
|
|
|
joinRequestedCallback = Callback<GameRichPresenceJoinRequested_t>.Create(OnJoinRequested);
|
|
avatarLoadedCallback = Callback<AvatarImageLoaded_t>.Create(OnAvatarLoaded);
|
|
callbacksReady = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the connect code into Steam Rich Presence (no-op when Steam/code is missing).
|
|
/// </summary>
|
|
private void PublishConnect(string code)
|
|
{
|
|
if (!SteamManager.Initialized || string.IsNullOrEmpty(code)) return;
|
|
SteamFriends.SetRichPresence(ConnectKey, code);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|