(Feat) Add Network Body
This commit is contained in:
@@ -38,6 +38,10 @@ namespace Ashwild.Network
|
||||
[Tooltip("The networked player prefab (NetworkObject + PlayerNetworkController).")]
|
||||
[SerializeField] private NetworkObject playerPrefab;
|
||||
|
||||
[Tooltip("Hard cap on total players in a room (host included). The transport is set to accept " +
|
||||
"maxPlayers - 1 remote clients, and the Steam invite lobby mirrors this limit.")]
|
||||
[SerializeField] private int maxPlayers = 6;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Logs every host/join/connection/spawn step to the console.")]
|
||||
[SerializeField] private bool verboseLogging = true;
|
||||
@@ -56,6 +60,12 @@ namespace Ashwild.Network
|
||||
/// </summary>
|
||||
public string GameSceneName => gameSceneName;
|
||||
|
||||
/// <summary>
|
||||
/// Hard cap on total players per room (host included). Single source of truth for the
|
||||
/// player limit — the Steam invite lobby reads this to mirror the cap.
|
||||
/// </summary>
|
||||
public int MaxPlayers => maxPlayers;
|
||||
|
||||
/// <summary>
|
||||
/// Cached FishNet manager resolved from the active scene.
|
||||
/// </summary>
|
||||
@@ -262,6 +272,8 @@ namespace Ashwild.Network
|
||||
|
||||
if (args.ConnectionState == LocalConnectionState.Started)
|
||||
{
|
||||
ApplyPlayerCap();
|
||||
|
||||
string code = GetLocalSteamCode();
|
||||
PlayerEvents.RaiseSessionStarted(code);
|
||||
Log($"✅ Session créée ! Code à partager = {code}");
|
||||
@@ -430,6 +442,20 @@ namespace Ashwild.Network
|
||||
PlayerEvents.RaiseMenuReady();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caps the transport at <see cref="maxPlayers"/> - 1 remote clients. Called once the server has
|
||||
/// Started, not before: FishySteamworks re-applies its own serialized limit to the socket inside
|
||||
/// StartConnection, so an earlier call would be clobbered. The host runs a local client that never
|
||||
/// crosses the Steam socket, so the remote limit is one below the total headcount; the transport
|
||||
/// then refuses the surplus connection outright.
|
||||
/// </summary>
|
||||
private void ApplyPlayerCap()
|
||||
{
|
||||
int remoteCap = Mathf.Max(0, maxPlayers - 1);
|
||||
networkManager.TransportManager.Transport.SetMaximumClients(remoteCap);
|
||||
Log($"Player cap set to {maxPlayers} (transport accepts {remoteCap} remote clients).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the gameplay scene as a global networked scene, replacing the menu.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using FishNet.Object;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// Splits the player's visible body by viewpoint. The local owner's third-person body is
|
||||
/// moved onto a dedicated "local body" layer that his own first-person camera excludes from
|
||||
/// its culling mask, so he never sees his own torso/head clip through the view — while every
|
||||
/// remote client still renders that same body normally. The layer change is purely local and
|
||||
/// never travels over the network, so hiding your body from yourself does not hide it from
|
||||
/// anyone else. The Animator/NetworkAnimator are left untouched, so the third-person animation
|
||||
/// keeps replicating to other players even while the mesh is culled from the owner's own view.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class PlayerBodyVisibility : NetworkBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Body")]
|
||||
[Tooltip("Root of the third-person body mesh hierarchy (skinned mesh + bones).")]
|
||||
[SerializeField] private GameObject bodyRoot;
|
||||
|
||||
[Header("Local hiding")]
|
||||
[Tooltip("Layer the owner's own body is moved to. The first-person camera must exclude this layer from its culling mask.")]
|
||||
[SerializeField] private string localBodyLayer = "LocalBody";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Once ownership is resolved, relayers the third-person body so the owning player's own
|
||||
/// camera stops rendering it. Remote copies return early and keep the body on its authored
|
||||
/// (visible) layer, which is why other players still see it.
|
||||
/// </summary>
|
||||
public override void OnStartClient()
|
||||
{
|
||||
base.OnStartClient();
|
||||
|
||||
if (!base.IsOwner) return;
|
||||
|
||||
if (bodyRoot == null)
|
||||
{
|
||||
Debug.LogError($"[PlayerBodyVisibility] '{name}' has no bodyRoot assigned — cannot hide the local body.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
int layer = LayerMask.NameToLayer(localBodyLayer);
|
||||
if (layer < 0)
|
||||
{
|
||||
Debug.LogError($"[PlayerBodyVisibility] '{name}' — layer '{localBodyLayer}' does not exist. Add it in Project Settings ▸ Tags and Layers, then exclude it from the first-person camera's culling mask.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
SetLayerRecursively(bodyRoot, layer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Applies a layer to a GameObject and every descendant, since a skinned body spans many
|
||||
/// child renderers and bones that must all move together for the camera cull to hide it fully.
|
||||
/// </summary>
|
||||
private static void SetLayerRecursively(GameObject root, int layer)
|
||||
{
|
||||
root.layer = layer;
|
||||
foreach (Transform child in root.transform)
|
||||
SetLayerRecursively(child.gameObject, layer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23848f2e7f7ecba4791aa89d5344b3d0
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace Ashwild.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// A tiny static hub for "another player joined / left my session" notifications, kept separate
|
||||
/// from PlayerEvents on purpose: PlayerEvents represents the LOCAL owned player and §3 forbids a
|
||||
/// remote player copy from driving it. These events are raised by SessionPresenceNotifier on the
|
||||
/// networked player copies (a local-client view of the session roster) and consumed by the game
|
||||
/// scene's presence feed UI. Names are already resolved display names, ready to show.
|
||||
/// </summary>
|
||||
public static class SessionPresenceEvents
|
||||
{
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// A remote player just joined the session — carries their display name.
|
||||
/// </summary>
|
||||
public static event Action<string> PlayerJoined;
|
||||
|
||||
/// <summary>
|
||||
/// A remote player just left the session — carries their display name.
|
||||
/// </summary>
|
||||
public static event Action<string> PlayerLeft;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Raisers
|
||||
|
||||
/// <summary>
|
||||
/// Announces that a remote player joined, to whoever is listening on this client.
|
||||
/// </summary>
|
||||
public static void RaisePlayerJoined(string playerName) => PlayerJoined?.Invoke(playerName);
|
||||
|
||||
/// <summary>
|
||||
/// Announces that a remote player left, to whoever is listening on this client.
|
||||
/// </summary>
|
||||
public static void RaisePlayerLeft(string playerName) => PlayerLeft?.Invoke(playerName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db47bb9b3f6b5c747ae09b007c8aebf0
|
||||
@@ -0,0 +1,97 @@
|
||||
using FishNet.Object;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// Announces this player's arrival and departure to every other client's presence feed. Lives on
|
||||
/// the networked player prefab (like PlayerNameTag) and is NOT owner-gated — remote copies must run
|
||||
/// it so they can report their own despawn as a "left".
|
||||
///
|
||||
/// Join is server-driven: the owner submits its Steam name, the server stores it and fires a
|
||||
/// one-shot ObserversRpc to whoever is watching right now. Because the RPC isn't buffered, a client
|
||||
/// joining a busy room never replays the joins that happened before it arrived — no startup burst.
|
||||
/// Leave is client-detected: when a player's copy despawns (disconnect), each remaining client reads
|
||||
/// the replicated name and reports it. The feed UI itself suppresses the departure storm that fires
|
||||
/// when the local player is the one leaving the session.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SessionPresenceNotifier : NetworkBehaviour
|
||||
{
|
||||
#region Networked State
|
||||
|
||||
/// <summary>
|
||||
/// The owner's display name, written by the server so every client can label this player's
|
||||
/// "left" message once the copy despawns and the owner is already gone.
|
||||
/// </summary>
|
||||
private readonly SyncVar<string> displayName = new SyncVar<string>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// On the owning client, publishes the local Steam persona name so the server can announce the join.
|
||||
/// </summary>
|
||||
public override void OnStartClient()
|
||||
{
|
||||
base.OnStartClient();
|
||||
|
||||
if (!base.IsOwner) return;
|
||||
|
||||
if (!SteamManager.Initialized)
|
||||
{
|
||||
Debug.LogError($"[SessionPresence] '{name}' — Steam not initialized, cannot announce join.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
SubmitPresenceServerRpc(SteamFriends.GetPersonaName());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A player's copy is despawning on this client. On non-owner copies that means a remote player
|
||||
/// left the session, so report it; the owner never reports its own departure. Gated on the local
|
||||
/// client still running so that OUR own disconnect — which despawns every copy at once — doesn't
|
||||
/// announce the whole room leaving.
|
||||
/// </summary>
|
||||
public override void OnStopClient()
|
||||
{
|
||||
base.OnStopClient();
|
||||
|
||||
if (base.IsOwner || !base.IsClientStarted) return;
|
||||
|
||||
string playerName = displayName.Value;
|
||||
if (!string.IsNullOrEmpty(playerName))
|
||||
SessionPresenceEvents.RaisePlayerLeft(playerName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Replication
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: records the owner's name and announces the join once to the current observers.
|
||||
/// </summary>
|
||||
[ServerRpc]
|
||||
private void SubmitPresenceServerRpc(string persona)
|
||||
{
|
||||
displayName.Value = persona;
|
||||
AnnounceJoinObserversRpc(persona);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires on every client observing this player at join time; each reports it to its feed,
|
||||
/// except the owner who shouldn't be told they joined their own session.
|
||||
/// </summary>
|
||||
[ObserversRpc]
|
||||
private void AnnounceJoinObserversRpc(string persona)
|
||||
{
|
||||
if (base.IsOwner) return;
|
||||
SessionPresenceEvents.RaisePlayerJoined(persona);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8809701d89694f5488cc03b300f8dc09
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FishNet;
|
||||
using FishNet.Managing;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
using Ashwild.Player;
|
||||
@@ -8,13 +10,18 @@ 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).
|
||||
/// • SEND — while in a session, keeps a private "carrier" Steam lobby (one per local player, all
|
||||
/// advertising the same host connect code) so the pause-menu "Invite" button can open Steam's
|
||||
/// lobby invite overlay. Any session member — host or client — can invite their own friends,
|
||||
/// and every invite still points at the single host's room (see NetworkSessionManager.MaxPlayers).
|
||||
/// • 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).
|
||||
/// friends see "Join Game" in their friends list and that path carries the code JoinSession reads.
|
||||
/// • RECEIVE — routes Steam's join paths to match the player's intent:
|
||||
/// – LobbyInvite_t (invitation just ARRIVED) → raises PlayerEvents.InviteReceived so the menu
|
||||
/// pops the invitation card; the player joins with one in-game click, no Alt-Tab to Steam.
|
||||
/// – GameLobbyJoinRequested_t / GameRichPresenceJoinRequested_t (player explicitly clicked
|
||||
/// "Join" inside Steam) → joins the session directly, skipping the card.
|
||||
/// Also resolves friend avatars (async via Steam) for the invitation card.
|
||||
/// Place this on the persistent FishNet NetworkManager GameObject, beside NetworkSessionManager.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
@@ -23,10 +30,16 @@ namespace Ashwild.Network
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// Steam Rich Presence key that makes the "Join Game" entry appear for friends.
|
||||
/// Steam Rich Presence / lobby-data key that makes the "Join Game" entry appear for friends
|
||||
/// and carries the connect code an incoming friend uses to reach our session.
|
||||
/// </summary>
|
||||
private const string ConnectKey = "connect";
|
||||
|
||||
/// <summary>
|
||||
/// Fallback member cap for the carrier lobby when the session manager is unavailable.
|
||||
/// </summary>
|
||||
private const int FallbackMemberLimit = 6;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
@@ -37,15 +50,41 @@ namespace Ashwild.Network
|
||||
public static SteamInviteService Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Steam callback fired when a friend accepts our invite or clicks "Join Game".
|
||||
/// Steam callback fired when a friend clicks "Join Game" via our Rich Presence connect string.
|
||||
/// </summary>
|
||||
private Callback<GameRichPresenceJoinRequested_t> joinRequestedCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Steam callback fired when the local player clicks "Join" on a lobby invite inside Steam.
|
||||
/// </summary>
|
||||
private Callback<GameLobbyJoinRequested_t> lobbyJoinRequestedCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Steam callback fired the moment a friend's lobby invitation ARRIVES (before any accept).
|
||||
/// </summary>
|
||||
private Callback<LobbyInvite_t> lobbyInviteCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Steam callback fired when an avatar image finishes downloading from Steam's servers.
|
||||
/// </summary>
|
||||
private Callback<AvatarImageLoaded_t> avatarLoadedCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Async result of CreateLobby — writes the connect code into the new lobby once it exists.
|
||||
/// </summary>
|
||||
private CallResult<LobbyCreated_t> lobbyCreatedResult;
|
||||
|
||||
/// <summary>
|
||||
/// The local player's carrier lobby while in a session (Nil when none). Exists only so the
|
||||
/// Steam overlay has a lobby to invite friends to; nobody actually stays in it.
|
||||
/// </summary>
|
||||
private CSteamID currentLobby;
|
||||
|
||||
/// <summary>
|
||||
/// Connect code to stamp onto the carrier lobby once CreateLobby completes.
|
||||
/// </summary>
|
||||
private string pendingLobbyCode;
|
||||
|
||||
/// <summary>
|
||||
/// Avatar requests still waiting on Steam to finish downloading the image, by SteamID64.
|
||||
/// </summary>
|
||||
@@ -86,7 +125,8 @@ namespace Ashwild.Network
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from the bus and disposes the Steam callbacks — mirrors OnEnable.
|
||||
/// Unsubscribes from the bus, tears down the carrier lobby, and disposes the Steam callbacks —
|
||||
/// mirrors OnEnable.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
@@ -94,10 +134,18 @@ namespace Ashwild.Network
|
||||
PlayerEvents.SessionJoined -= HandleSessionJoined;
|
||||
PlayerEvents.SessionStopped -= HandleSessionStopped;
|
||||
|
||||
LeaveCarrierLobby();
|
||||
|
||||
joinRequestedCallback?.Dispose();
|
||||
lobbyJoinRequestedCallback?.Dispose();
|
||||
lobbyInviteCallback?.Dispose();
|
||||
avatarLoadedCallback?.Dispose();
|
||||
lobbyCreatedResult?.Dispose();
|
||||
joinRequestedCallback = null;
|
||||
lobbyJoinRequestedCallback = null;
|
||||
lobbyInviteCallback = null;
|
||||
avatarLoadedCallback = null;
|
||||
lobbyCreatedResult = null;
|
||||
callbacksReady = false;
|
||||
|
||||
pendingAvatars.Clear();
|
||||
@@ -112,10 +160,11 @@ namespace Ashwild.Network
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the singleton reference.
|
||||
/// Clears the singleton reference and leaves any carrier lobby still open.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
LeaveCarrierLobby();
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
@@ -124,8 +173,10 @@ namespace Ashwild.Network
|
||||
#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.
|
||||
/// Opens the Steam overlay so the player can invite a friend to the current session. Prefers
|
||||
/// the lobby invite dialog (so the friend gets an in-game invitation card via LobbyInvite_t);
|
||||
/// falls back to the plain connect-string dialog if the carrier lobby isn't ready. Requires
|
||||
/// being in a session (host or client) so there is a code to share.
|
||||
/// </summary>
|
||||
public void OpenInviteOverlay()
|
||||
{
|
||||
@@ -142,7 +193,10 @@ namespace Ashwild.Network
|
||||
return;
|
||||
}
|
||||
|
||||
SteamFriends.ActivateGameOverlayInviteDialogConnectString(code);
|
||||
if (currentLobby.IsValid())
|
||||
SteamFriends.ActivateGameOverlayInviteDialog(currentLobby);
|
||||
else
|
||||
SteamFriends.ActivateGameOverlayInviteDialogConnectString(code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,7 +235,44 @@ namespace Ashwild.Network
|
||||
#region Steam Callbacks
|
||||
|
||||
/// <summary>
|
||||
/// A friend accepted our invite / clicked "Join Game" — surface it to the menu popup.
|
||||
/// A friend's lobby invitation just ARRIVED — surface it to the menu popup so the player can
|
||||
/// join with one in-game click. The connect code is read from the inviter's Rich Presence,
|
||||
/// which every session member publishes; an empty read means we can't build a joinable card.
|
||||
/// </summary>
|
||||
private void OnLobbyInvite(LobbyInvite_t cb)
|
||||
{
|
||||
CSteamID inviter = new CSteamID(cb.m_ulSteamIDUser);
|
||||
string connect = ResolveConnectFromFriend(inviter);
|
||||
if (string.IsNullOrEmpty(connect))
|
||||
{
|
||||
Debug.LogWarning($"[SteamInvite] Lobby invite from {cb.m_ulSteamIDUser} had no readable connect code — ignoring.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
string inviterName = inviter.IsValid() ? SteamFriends.GetFriendPersonaName(inviter) : "Un ami";
|
||||
Debug.Log($"[SteamInvite] Invitation reçue de {inviterName} (connect={connect}).", this);
|
||||
PlayerEvents.RaiseInviteReceived(cb.m_ulSteamIDUser, inviterName, connect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The player clicked "Join" on a lobby invite inside Steam — an explicit intent, so we join
|
||||
/// the session directly and skip the invitation card.
|
||||
/// </summary>
|
||||
private void OnLobbyJoinRequested(GameLobbyJoinRequested_t cb)
|
||||
{
|
||||
string connect = ResolveConnectFromFriend(cb.m_steamIDFriend);
|
||||
if (string.IsNullOrEmpty(connect))
|
||||
{
|
||||
Debug.LogWarning("[SteamInvite] Lobby join request had no readable connect code — ignoring.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
JoinDirect(connect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A friend clicked "Join Game" via our Rich Presence connect string — an explicit intent, so
|
||||
/// we join directly rather than popping the invitation card.
|
||||
/// </summary>
|
||||
private void OnJoinRequested(GameRichPresenceJoinRequested_t cb)
|
||||
{
|
||||
@@ -192,13 +283,24 @@ namespace Ashwild.Network
|
||||
return;
|
||||
}
|
||||
|
||||
ulong inviterId = cb.m_steamIDFriend.m_SteamID;
|
||||
string inviterName = cb.m_steamIDFriend.IsValid()
|
||||
? SteamFriends.GetFriendPersonaName(cb.m_steamIDFriend)
|
||||
: "Un ami";
|
||||
JoinDirect(connect);
|
||||
}
|
||||
|
||||
Debug.Log($"[SteamInvite] Invitation reçue de {inviterName} (connect={connect}).", this);
|
||||
PlayerEvents.RaiseInviteReceived(inviterId, inviterName, connect);
|
||||
/// <summary>
|
||||
/// The carrier lobby finished creating — record it and stamp the connect code onto it so a
|
||||
/// joining friend could also recover the code from lobby data if needed.
|
||||
/// </summary>
|
||||
private void OnLobbyCreated(LobbyCreated_t cb, bool ioFailure)
|
||||
{
|
||||
if (ioFailure || cb.m_eResult != EResult.k_EResultOK)
|
||||
{
|
||||
Debug.LogWarning($"[SteamInvite] Failed to create the invite lobby (result={cb.m_eResult}, ioFailure={ioFailure}). Invites fall back to the connect string.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
currentLobby = new CSteamID(cb.m_ulSteamIDLobby);
|
||||
if (!string.IsNullOrEmpty(pendingLobbyCode))
|
||||
SteamMatchmaking.SetLobbyData(currentLobby, ConnectKey, pendingLobbyCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,22 +320,34 @@ namespace Ashwild.Network
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Host came online — advertise the session code so friends can join/invite.
|
||||
/// Host came online — advertise the session code and open a carrier lobby friends can be invited to.
|
||||
/// </summary>
|
||||
private void HandleSessionOnline(string code) => PublishConnect(code);
|
||||
private void HandleSessionOnline(string code)
|
||||
{
|
||||
PublishConnect(code);
|
||||
CreateCarrierLobby(code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joined a host — advertise the same code so this client can also invite friends.
|
||||
/// Joined a host — advertise the same code and open a carrier lobby so this client can also
|
||||
/// invite its own friends into the host's room.
|
||||
/// </summary>
|
||||
private void HandleSessionJoined() => PublishConnect(PlayerEvents.SessionCode);
|
||||
private void HandleSessionJoined()
|
||||
{
|
||||
string code = PlayerEvents.SessionCode;
|
||||
PublishConnect(code);
|
||||
CreateCarrierLobby(code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Session ended — remove the Rich Presence so we stop advertising as joinable.
|
||||
/// Session ended — stop advertising as joinable and tear down the carrier lobby.
|
||||
/// </summary>
|
||||
private void HandleSessionStopped()
|
||||
{
|
||||
if (SteamManager.Initialized)
|
||||
SteamFriends.SetRichPresence(ConnectKey, null);
|
||||
|
||||
LeaveCarrierLobby();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -248,7 +362,10 @@ namespace Ashwild.Network
|
||||
if (callbacksReady || !SteamManager.Initialized) return;
|
||||
|
||||
joinRequestedCallback = Callback<GameRichPresenceJoinRequested_t>.Create(OnJoinRequested);
|
||||
lobbyJoinRequestedCallback = Callback<GameLobbyJoinRequested_t>.Create(OnLobbyJoinRequested);
|
||||
lobbyInviteCallback = Callback<LobbyInvite_t>.Create(OnLobbyInvite);
|
||||
avatarLoadedCallback = Callback<AvatarImageLoaded_t>.Create(OnAvatarLoaded);
|
||||
lobbyCreatedResult = CallResult<LobbyCreated_t>.Create(OnLobbyCreated);
|
||||
callbacksReady = true;
|
||||
}
|
||||
|
||||
@@ -261,6 +378,88 @@ namespace Ashwild.Network
|
||||
SteamFriends.SetRichPresence(ConnectKey, code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens (or refreshes) the private carrier lobby that the invite overlay hangs its friend
|
||||
/// invites off. Friends-only and capped at the room's player limit; the connect code is stamped
|
||||
/// on once LobbyCreated returns. No-op without Steam or a code; refreshes data if one already exists.
|
||||
/// </summary>
|
||||
private void CreateCarrierLobby(string code)
|
||||
{
|
||||
if (!SteamManager.Initialized || string.IsNullOrEmpty(code)) return;
|
||||
|
||||
pendingLobbyCode = code;
|
||||
|
||||
if (currentLobby.IsValid())
|
||||
{
|
||||
SteamMatchmaking.SetLobbyData(currentLobby, ConnectKey, code);
|
||||
return;
|
||||
}
|
||||
|
||||
int memberLimit = NetworkSessionManager.Instance != null
|
||||
? NetworkSessionManager.Instance.MaxPlayers
|
||||
: FallbackMemberLimit;
|
||||
|
||||
SteamAPICall_t call = SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypeFriendsOnly, memberLimit);
|
||||
lobbyCreatedResult?.Set(call);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leaves and forgets the carrier lobby, if one is open.
|
||||
/// </summary>
|
||||
private void LeaveCarrierLobby()
|
||||
{
|
||||
pendingLobbyCode = null;
|
||||
if (!currentLobby.IsValid()) return;
|
||||
|
||||
if (SteamManager.Initialized)
|
||||
SteamMatchmaking.LeaveLobby(currentLobby);
|
||||
currentLobby = CSteamID.Nil;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the connect code a session member publishes via Rich Presence. Returns empty when the
|
||||
/// friend is invalid, offline, or hasn't published one (they may not be in a session).
|
||||
/// </summary>
|
||||
private string ResolveConnectFromFriend(CSteamID friend)
|
||||
{
|
||||
if (!SteamManager.Initialized || !friend.IsValid()) return string.Empty;
|
||||
return SteamFriends.GetFriendRichPresence(friend, ConnectKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects straight to a session by its code — used when the player expressed an explicit
|
||||
/// "Join" intent inside Steam. Ignored (with a log) when already in a session so a stray Steam
|
||||
/// click can't tear down a running game.
|
||||
/// </summary>
|
||||
private void JoinDirect(string code)
|
||||
{
|
||||
if (IsAlreadyInSession())
|
||||
{
|
||||
Debug.LogWarning("[SteamInvite] Ignoring a Steam join request — already in a session.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (NetworkSessionManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("[SteamInvite] Cannot join — NetworkSessionManager missing.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
GameSession.Mode = GameSession.LaunchMode.Join;
|
||||
GameSession.JoinCode = code;
|
||||
NetworkSessionManager.Instance.JoinSession(code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when a client or server connection is already up, so we shouldn't start a fresh join.
|
||||
/// </summary>
|
||||
private bool IsAlreadyInSession()
|
||||
{
|
||||
NetworkManager nm = InstanceFinder.NetworkManager;
|
||||
if (nm == null) return false;
|
||||
return nm.ClientManager.Started || nm.ServerManager.Started;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user