using Unity.Netcode; using UnityEngine; namespace Jankenbots.Prototype { /// /// SeatManager — decides WHICH player drives WHICH tread of the shared bot. /// /// M1 coordination feel: two humans each pilot half of one janky body, so the /// whole point is that a seat is a scarce, claimable slot. This lives on the /// bot chassis GameObject (alongside the chassis Rigidbody / NetworkObject) and /// is the single source of truth for seat ownership. /// /// Model: /// - Two seats, Left and Right. /// - Each seat stores the ulong clientId of its pilot, or /// (== NEUTRAL / free) when nobody holds it. /// - First player to ask claims Left, the next claims Right, everyone after /// that gets nothing (a spectator for M1). Treads with no pilot stay NEUTRAL /// and simply apply no force — the bot just doesn't move on that side. /// /// Authority: server-only writes. Seat state is host-authoritative just like the /// physics, so clients never guess — they read replicated NetworkVariables and /// send a ServerRpc to *ask* for a seat. /// /// TreadPart hooks into this via : a TreadPart is tagged /// Left or Right, and on the host it only honors input from the client that /// currently owns that seat. /// [RequireComponent(typeof(NetworkObject))] public class SeatManager : NetworkBehaviour { public enum Seat { Left, Right } // Sentinel for "this seat is empty / NEUTRAL". Real clientIds are small // ulongs starting at 0, so MaxValue is a safe "nobody" marker. We can't use // -1 because clientId is unsigned. public const ulong NoPilot = ulong.MaxValue; // Replicated seat ownership. Server writes, everyone reads. Clients subscribe // to OnValueChanged if they want to react (e.g. update the seat-claim UI); // for M1 we just poll them in OnGUI, which is plenty. public readonly NetworkVariable LeftPilot = new NetworkVariable(NoPilot, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); public readonly NetworkVariable RightPilot = new NetworkVariable(NoPilot, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); // --------------------------------------------------------------------- // Leaver-safety wiring // --------------------------------------------------------------------- // If a pilot rage-quits (or their WiFi dies), their seat MUST free up so a // remaining/new player can grab it — otherwise the bot is permanently // half-dead. We hook client-disconnect on the server and clear any seat that // pointed at the departing client. public override void OnNetworkSpawn() { // Only the server owns seat state, so only the server needs the hook. if (IsServer) { NetworkManager.Singleton.OnClientDisconnectCallback += HandleClientDisconnect; } } public override void OnNetworkDespawn() { if (IsServer && NetworkManager.Singleton != null) { NetworkManager.Singleton.OnClientDisconnectCallback -= HandleClientDisconnect; } } // Leaver-safety: free whichever seat the departing pilot held. void HandleClientDisconnect(ulong clientId) { if (LeftPilot.Value == clientId) LeftPilot.Value = NoPilot; if (RightPilot.Value == clientId) RightPilot.Value = NoPilot; } // --------------------------------------------------------------------- // Claiming a seat // --------------------------------------------------------------------- // A client presses "join the bot"; we don't let it pick a side — first come // gets Left, second gets Right. This keeps M1 dead simple and mirrors the // "first two players claim left/right tread" scope. /// /// Client asks the host for a seat. RequireOwnership is false because the /// asking client does NOT own the bot (the host does) — any client may ask. /// The sender's clientId comes from RpcParams, so a client can't lie about /// who it is. /// [Rpc(SendTo.Server, RequireOwnership = false)] public void ClaimSeatRpc(RpcParams rpcParams = default) { ulong requester = rpcParams.Receive.SenderClientId; // Already seated? Do nothing (idempotent — safe to spam the button). if (LeftPilot.Value == requester || RightPilot.Value == requester) return; // Fill Left first, then Right. Anyone after that is a spectator for M1. if (LeftPilot.Value == NoPilot) LeftPilot.Value = requester; else if (RightPilot.Value == NoPilot) RightPilot.Value = requester; // else: bot is full — silently ignore. } /// /// Voluntarily give up your seat (frees it as NEUTRAL). Handy for testing and /// for letting a player hop from Left to Right. Server-authoritative. /// [Rpc(SendTo.Server, RequireOwnership = false)] public void ReleaseSeatRpc(RpcParams rpcParams = default) { ulong requester = rpcParams.Receive.SenderClientId; if (LeftPilot.Value == requester) LeftPilot.Value = NoPilot; if (RightPilot.Value == requester) RightPilot.Value = NoPilot; } // --------------------------------------------------------------------- // Queries used by TreadPart (host-side input gating) // --------------------------------------------------------------------- /// Does currently pilot ? /// TreadPart calls this on the host to decide whether to honor an input RPC. public bool OwnsSeat(Seat seat, ulong clientId) { return PilotOf(seat) == clientId && clientId != NoPilot; } /// ClientId piloting a seat, or if free. public ulong PilotOf(Seat seat) { return seat == Seat.Left ? LeftPilot.Value : RightPilot.Value; } /// Which seat (if any) the local player holds — for local UI only. Seat? LocalSeat() { if (NetworkManager.Singleton == null) return null; ulong me = NetworkManager.Singleton.LocalClientId; if (LeftPilot.Value == me) return Seat.Left; if (RightPilot.Value == me) return Seat.Right; return null; } // --------------------------------------------------------------------- // Throwaway debug UI — good enough for playtesting with friends. // --------------------------------------------------------------------- void OnGUI() { if (!IsSpawned) return; Seat? mine = LocalSeat(); string held = mine.HasValue ? mine.Value.ToString() : "none (spectator)"; // Sits BELOW the NetworkBootstrap connectivity panel (which occupies the // top-left ~260px) so the Claim button isn't hidden behind it. const int x = 10, y = 300, w = 260, h = 78; GUI.Box(new Rect(x, y, w, h), "JANKENBOTS · seat"); GUI.Label(new Rect(x + 10, y + 22, w - 20, 20), $"You hold: {held}"); GUI.Label(new Rect(x + 10, y + 42, w - 20, 20), $"Left: {SeatLabel(LeftPilot.Value)} Right: {SeatLabel(RightPilot.Value)}"); // Client-side ask/release buttons (they just fire the ServerRpc). if (!mine.HasValue) { if (GUI.Button(new Rect(x + w - 70, y + 20, 55, 20), "Claim")) ClaimSeatRpc(); } else if (GUI.Button(new Rect(x + w - 70, y + 20, 55, 20), "Leave")) { ReleaseSeatRpc(); } } static string SeatLabel(ulong pilot) => pilot == NoPilot ? "free" : $"#{pilot}"; } }