// Backpack.cs // A sandbox backpack: every player gets a "Backpack" button on their control bar. It opens a panel of the // items you put in the lists below; picking one spawns it in front of that player (on the server, for // everyone). Items can be meshes or prefabs: rigidbodies fall and can be grabbed, scripts on them run. // // Fill the three lists in the Inspector: entry #0 of each list is item #0, entry #1 is item #1, and so on. // items - what to spawn: drag prefabs, meshes or system objects from the Inventory panel onto the list // icons - drag a texture for each item's picture (none: the item's own thumbnail) // names - the name shown under each picture (left empty: the item's own name) // When At Limit chooses what happens when a player already has Max Items Per User out: recycle their // oldest, refuse the new one, or replace them all (or write your own rule in MakeRoom). using System.Collections; using System.Collections.Generic; using IHXR.Simulation; public class Backpack : ScriptBase { // What happens when a player picks an item while they already have Max Items Per User spawned. public enum WhenAtLimit { RecycleOldest, // their oldest item is removed, then the new one spawns RefuseNewItem, // nothing spawns; the player is told they are at the limit ReplaceAll // all their items are removed, then the new one spawns } [Header("Backpack items (entry #N of each list belongs together)")] public List items = new List(); public List icons = new List(); public List names = new List(); [Header("Limits")] [Tooltip("How many spawned items one player can have at once. Spawning another removes their oldest.")] [Range(1, 50)] public int maxItemsPerUser = 5; [Tooltip("What happens when a player is at the limit and picks another item.")] public WhenAtLimit whenAtLimit = WhenAtLimit.RecycleOldest; [Tooltip("Remove each spawned item after a while.")] public bool autoDestroy = true; [Tooltip("Seconds before a spawned item is removed (when Auto Destroy is on).")] [Range(5, 600)] public float autoDestroySeconds = 60f; [Tooltip("Remove a player's items when they leave the world.")] public bool destroyWhenPlayerLeaves = true; [Header("Placement")] [Tooltip("Metres in front of the player.")] public float spawnDistance = 1.5f; [Tooltip("Metres above the player's feet (dynamic items drop from here).")] public float spawnHeight = 1f; [Tooltip("Turn each spawned item to face the way the player faces.")] public bool faceThePlayersWay = true; private readonly Dictionary> spawnedByUser = new Dictionary>(); public override void Start() { if (items == null || items.Count == 0) { Log("[Backpack] Add items to the Items list in the Inspector."); return; } SetupBackpack(items, icons, names, OnItemPicked); } private void OnItemPicked(BackpackSelection pick) { List mine = ItemsOf(pick.Username); int limit = Mathf.Max(1, maxItemsPerUser); if (mine.Count >= limit && !MakeRoom(pick, mine, limit)) return; Vector3 position = pick.SpawnPosition(spawnDistance, spawnHeight); Vector3 rotation = faceThePlayersWay ? new Vector3(0f, pick.Yaw, 0f) : Vector3.zero; SceneObject spawned = SpawnInventoryItem(pick.Item, position, rotation, Vector3.one); if (spawned == null || spawned.IsNull) { Log($"[Backpack] Could not spawn {pick.DisplayName}."); return; } var entry = new SpawnedItem { Object = spawned }; mine.Add(entry); if (autoDestroy && autoDestroySeconds > 0f) entry.Timeout = StartCoroutine(DespawnAfter(pick.Username, entry, autoDestroySeconds)); } // At the limit: returns true when the new item may spawn. Change this to make your own rule // (for example: only remove items of the same kind, or charge coins for more). private bool MakeRoom(BackpackSelection pick, List mine, int limit) { switch (whenAtLimit) { case WhenAtLimit.RefuseNewItem: ShowDialog("Backpack", $"You already have {limit} item{(limit == 1 ? "" : "s")} out. Wait for one to disappear first.", pick.Username, ModalDialogIcon.Info); return false; case WhenAtLimit.ReplaceAll: DespawnAllOf(pick.Username); return true; default: // RecycleOldest while (mine.Count >= limit) Despawn(pick.Username, mine[0]); return true; } } private IEnumerator DespawnAfter(string username, SpawnedItem entry, float seconds) { yield return new WaitForSeconds(seconds); entry.Timeout = null; Despawn(username, entry); } private List ItemsOf(string username) { string key = (username ?? "").ToLowerInvariant(); if (!spawnedByUser.TryGetValue(key, out List list)) { list = new List(); spawnedByUser[key] = list; } return list; } private void Despawn(string username, SpawnedItem entry) { if (entry == null) return; if (entry.Timeout != null) StopCoroutine(entry.Timeout); ItemsOf(username).Remove(entry); if (entry.Object != null && !entry.Object.IsNull) DespawnSpawnedObject(entry.Object); } private void DespawnAllOf(string username) { List mine = ItemsOf(username); for (int i = mine.Count - 1; i >= 0; i--) Despawn(username, mine[i]); } public override void OnUserPart(UserInfo user) { if (destroyWhenPlayerLeaves && user != null) DespawnAllOf(user.Username); } public override void OnDestroy() { foreach (string username in new List(spawnedByUser.Keys)) DespawnAllOf(username); RemoveBackpack(); } private class SpawnedItem { public SceneObject Object; public Coroutine Timeout; } }