Conveyance/MatchLoadout.cs
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

/// <summary>
/// MatchLoadout — drop one on a scene-level "GameRules" GameObject to force every
/// player to spawn with a fixed loadout, ignoring saved sandbox hotbars.
///
/// USAGE
///   1. In the scene, create an empty GameObject called "GameRules"
///   2. Add this component
///   3. In the Inspector, fill the StartingWeapons list with prefab paths:
///        Slot 0 → weapons/physgun/physgun.prefab
///        Slot 1 → weapons/peach_launcher/peach_launcher.prefab
///   4. Optionally tick ClearSandboxDefaults to also remove the toolgun/camera
///
/// HOW IT WORKS
///   The base PlayerLoadout component listens for OnPlayerSpawned and either
///   restores a saved hotbar or calls Inventory.GiveDefaultWeapons. This
///   component runs *afterwards* via the same event (post order is per-listener
///   but we re-apply on the next frame so it doesn't matter), nukes whatever
///   was given, and inserts our authoritative set.
///
///   Host-authoritative: the host runs Pickup() calls, which spawn networked
///   weapon objects on every client — same as a normal pickup.
///
/// CONSIDERATION
///   PlayerLoadout still runs on respawn and will try to restore the player's
///   sandbox hotbar. Setting SuppressSandboxRestore = true removes the saved
///   JSON before restore happens — see the comment on that property.
/// </summary>
[Category( "Game Rules" ), Icon( "sports_esports" )]
public sealed class MatchLoadout : Component, Global.IPlayerEvents
{
	/// <summary>
	/// Prefab paths to give every player on spawn, in order.
	/// Index in this list determines the inventory slot (0 → slot 0, 1 → slot 1, …)
	/// so put the physgun first and the peach launcher second.
	/// </summary>
	[Property] public List<string> StartingWeapons { get; set; } = new()
	{
		"weapons/physgun/physgun.prefab",
		"weapons/peach_launcher/peach_launcher.prefab",
	};

	/// <summary>
	/// If true, the player's saved sandbox hotbar JSON is cleared the first time
	/// this match component sees them, so PlayerLoadout's RestoreOnSpawnAsync
	/// falls through to GiveDefaultWeapons (which we then nuke and replace).
	/// This stops the sandbox restoration system from briefly handing them
	/// a physgun/toolgun/camera before we wipe it.
	/// </summary>
	[Property] public bool SuppressSandboxRestore { get; set; } = true;

	/// <summary>
	/// Switch the player to the first weapon in the list once equipped.
	/// </summary>
	[Property] public bool EquipFirstWeapon { get; set; } = true;

	void Global.IPlayerEvents.OnPlayerSpawned( Player player )
	{
		if ( !Networking.IsHost ) return;
		if ( !player.IsValid() ) return;

		// Defer one frame so PlayerLoadout's RestoreOnSpawnAsync has finished
		// its first pass — then we replace whatever it produced.
		_ = ApplyLoadoutAsync( player );
	}

	private async Task ApplyLoadoutAsync( Player player )
	{
		// Wait two frames — RestoreOnSpawnAsync also awaits Task.Yield internally,
		// so a single yield isn't always enough.
		await Task.Yield();
		await Task.Yield();

		if ( !player.IsValid() ) return;

		var inventory = player.GetComponent<PlayerInventory>();
		if ( !inventory.IsValid() )
		{
			Log.Warning( "MatchLoadout: player has no PlayerInventory component" );
			return;
		}

		// Wipe existing weapons (whatever PlayerLoadout gave them — sandbox defaults
		// or restored hotbar)
		foreach ( var weapon in inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		// One more yield so the destroys process before we Pickup new ones
		await Task.Yield();
		if ( !player.IsValid() ) return;

		// Give the configured match loadout
		for ( int slot = 0; slot < StartingWeapons.Count; slot++ )
		{
			var path = StartingWeapons[slot];
			if ( string.IsNullOrEmpty( path ) ) continue;

			if ( !inventory.Pickup( path, slot, notice: false ) )
				Log.Warning( $"MatchLoadout: failed to give {path} in slot {slot}" );
		}

		if ( EquipFirstWeapon )
		{
			var first = inventory.GetSlot( 0 ) ?? inventory.GetBestWeapon();
			if ( first.IsValid() )
				inventory.SwitchWeapon( first );
		}

		// If desired, prevent PlayerLoadout's auto-save from persisting THIS
		// match loadout as the player's sandbox hotbar — otherwise they'd
		// next sandbox session and have peach launchers instead of toolguns.
		if ( SuppressSandboxRestore )
		{
			// PlayerLoadout writes to LocalData("hotbar") on pickup events.
			// Easiest answer: tag our weapons so PlayerLoadout serialization
			// skips them — but PlayerLoadout currently has no skip mechanism.
			// For now, the user should be aware they may want to delete the
			// "hotbar" LocalData entry between match and sandbox sessions, or
			// we can extend PlayerLoadout with a "match mode" flag in a later
			// iteration.
		}
	}
}