using System; using System.Collections.Generic; using System.Linq; using Facepunch; using Oxide.Game.Rust.Cui; using UnityEngine; using UnityEngine.UI; namespace Oxide.Plugins { [Info("TestLoot", "Uzumi", "1.0.0")] [Description("Opens a UI to spawn loot containers; after 5 seconds ejects their loot and removes the containers.")] internal class TestLoot : RustPlugin { private const string PermissionUse = "testloot.use"; private const string MainPanelName = "TestLoot_Main"; private const string OverlayName = "TestLoot_Overlay"; private const string ScrollContainerName = "TestLoot_ScrollContainer"; private const string ScrollViewName = "TestLoot_ScrollView"; private const string AmountPanelName = "TestLoot_AmountPanel"; private const string SpawnPanelName = "TestLoot_SpawnPanel"; private const string ConsoleCommandPrefix = "testloot"; private const float EjectRadius = 3f; private const float EjectDelaySeconds = 5f; private const float SpawnLookDistance = 8f; private const float SpawnHeightOffset = 0.4f; private const int RowHeightPx = 28; private const int RowSpacingPx = 8; private const int GridColumns = 3; private const float GridColumnGap = 0.012f; private const float GridCellPadding = 0.04f; private static readonly string[] LootContainers = new[] { "codelockedhackablecrate", "codelockedhackablecrate_ghostship", "codelockedhackablecrate_oilrig", "crate_basic", "crate_basic_jungle", "crate_cannons", "crate_elite", "crate_elite_tutorial", "crate_mine", "crate_normal", "crate_normal_2", "crate_normal_2_food", "crate_normal_2_medical", "crate_tools", "crate_underwater_advanced", "crate_underwater_basic", "foodbox", "loot-barrel-1", "loot-barrel-2", "loot_barrel_1", "loot_barrel_2", "minecart", "oil_barrel", "presentdrop", "supply_drop", "trash-pile-1", "underwater_labs/crate_ammunition", "underwater_labs/crate_elite", "underwater_labs/crate_food_1", "underwater_labs/crate_food_2", "underwater_labs/crate_fuel", "underwater_labs/crate_medical", "underwater_labs/crate_normal", "underwater_labs/crate_normal_2", "underwater_labs/crate_tools", "underwater_labs/tech_parts_1", "underwater_labs/tech_parts_2", "underwater_labs/vehicle_parts", "vehicle_parts" }; private Dictionary _playerSelection = new Dictionary(); private List _ejectTimers = new List(); private void Init() { permission.RegisterPermission(PermissionUse, this); cmd.AddChatCommand("tl", this, nameof(CmdTestLoot)); } private void Unload() { foreach (var player in BasePlayer.activePlayerList) CloseTestLootUi(player); foreach (var t in _ejectTimers) t?.Destroy(); _ejectTimers.Clear(); _ejectTimers = null; _playerSelection?.Clear(); _playerSelection = null; } private void OnPlayerDisconnected(BasePlayer player, string reason) { if (player == null) return; CloseTestLootUi(player); _playerSelection?.Remove(player.userID); } private void CmdTestLoot(BasePlayer player) { if (player == null) return; if (!permission.UserHasPermission(player.UserIDString, PermissionUse) && !player.IsAdmin) { SendReply(player, "You don't have permission to use TestLoot."); return; } CloseTestLootUi(player); EnsureSelection(player); OpenTestLootUi(player); } [ConsoleCommand(ConsoleCommandPrefix + ".close")] private void CmdConsoleClose(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseTestLootUi(player); _playerSelection?.Remove(player.userID); } [ConsoleCommand(ConsoleCommandPrefix + ".spawn")] private void CmdConsoleSpawn(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; if (!permission.UserHasPermission(player.UserIDString, PermissionUse) && !player.IsAdmin) return; EnsureSelection(player); if (!_playerSelection.TryGetValue(player.userID, out var sel)) { SendReply(player, "Select a container first."); return; } int amount = Mathf.Clamp(sel.amount, 1, 200); string prefabPath = GetPrefabPath(sel.container); if (string.IsNullOrEmpty(prefabPath)) { SendReply(player, $"Prefab not found for: {sel.container}"); return; } Vector3 center = GetSpawnPosition(player); SpawnContainers(prefabPath, center, amount); timer.Once(0.2f, () => { TriggerSpawnLoot(center, EjectRadius); }); var ejectTimer = timer.Once(EjectDelaySeconds, () => EjectLootAt(center, EjectRadius)); _ejectTimers?.RemoveAll(t => t == null || !t.Destroyed); _ejectTimers?.Add(ejectTimer); SendReply(player, $"Spawning {amount} x {sel.container}. Loot will eject in {EjectDelaySeconds}s."); } [ConsoleCommand(ConsoleCommandPrefix + ".select")] private void CmdConsoleSelect(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; int index; if (arg.Args == null || arg.Args.Length < 1 || !int.TryParse(arg.Args[0], out index) || index < 0 || index >= LootContainers.Length) return; EnsureSelection(player); var sel = _playerSelection[player.userID]; _playerSelection[player.userID] = (LootContainers[index], sel.amount); } [ConsoleCommand(ConsoleCommandPrefix + ".amount")] private void CmdConsoleAmount(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; int amount; if (arg.Args == null || arg.Args.Length < 1 || !int.TryParse(arg.Args[0], out amount)) return; amount = Mathf.Clamp(amount, 1, 200); EnsureSelection(player); var sel = _playerSelection[player.userID]; _playerSelection[player.userID] = (sel.container, amount); } private void EnsureSelection(BasePlayer player) { if (player == null) return; if (_playerSelection == null) _playerSelection = new Dictionary(); if (!_playerSelection.ContainsKey(player.userID)) _playerSelection[player.userID] = (LootContainers[0], 1); } private static Vector3 GetSpawnPosition(BasePlayer player) { if (player == null) return Vector3.zero; if (Physics.Raycast(player.eyes.HeadRay(), out RaycastHit hit, SpawnLookDistance)) return hit.point + Vector3.up * SpawnHeightOffset; return player.transform.position + player.eyes.BodyForward() * 2f + Vector3.up * SpawnHeightOffset; } private static string GetPrefabPath(string shortName) { if (string.IsNullOrEmpty(shortName)) return null; if (GameManifest.Current?.entities == null) return null; string suffix = shortName + ".prefab"; string suffixLower = suffix.ToLowerInvariant(); int lastSlash = shortName.LastIndexOf('/'); string segmentSuffix = lastSlash >= 0 && lastSlash < shortName.Length - 1 ? shortName.Substring(lastSlash + 1) + ".prefab" : null; string segmentLower = segmentSuffix?.ToLowerInvariant(); for (int i = 0; i < GameManifest.Current.entities.Length; i++) { string path = GameManifest.Current.entities[i]; if (string.IsNullOrEmpty(path)) continue; string pathLower = path.ToLowerInvariant(); if (pathLower.Contains("invisible")) continue; if (pathLower.EndsWith(suffixLower, StringComparison.OrdinalIgnoreCase)) return path; if (segmentLower != null && pathLower.EndsWith(segmentLower, StringComparison.OrdinalIgnoreCase)) return path; } return null; } private void SpawnContainers(string prefabPath, Vector3 center, int amount) { for (int i = 0; i < amount; i++) { float xOff = (i % 10) * 0.025f; float zOff = (i / 10) * 0.025f; float yOff = i * 0.015f; Vector3 pos = center + new Vector3(xOff, yOff, zOff); BaseEntity entity = GameManager.server.CreateEntity(prefabPath, pos, Quaternion.identity); if (entity != null) { entity.Spawn(); } } } private void TriggerSpawnLoot(Vector3 center, float radius) { var ents = Pool.Get>(); try { Vis.Entities(center, radius, ents); foreach (var e in ents) { if (e is LootContainer lc && !lc.IsDestroyed && lc.inventory != null) { lc.CancelInvoke(lc.SpawnLoot); lc.Invoke(lc.SpawnLoot, 0.15f); } } } finally { Pool.FreeUnmanaged(ref ents); } } private void EjectLootAt(Vector3 center, float radius) { var ents = Pool.Get>(); try { Vis.Entities(center, radius, ents); foreach (var e in ents) { if (e == null || e.IsDestroyed) continue; if (!(e is LootContainer lc)) continue; if (lc.inventory == null || lc.inventory.itemList == null) continue; List copy; try { copy = lc.inventory.itemList.ToList(); } catch { copy = new List(); } Vector3 dropPos = lc.inventory.dropPosition; Vector3 dropVel = lc.inventory.dropVelocity; foreach (var item in copy) { if (item == null || item.amount < 1) continue; item.RemoveFromContainer(); item.Drop(dropPos + UnityEngine.Random.insideUnitSphere * 0.2f, dropVel); } lc.Kill(); } } finally { Pool.FreeUnmanaged(ref ents); } _ejectTimers?.RemoveAll(t => t == null || t.Destroyed); } private void CloseTestLootUi(BasePlayer player) { if (player == null || !player.IsConnected) return; CuiHelper.DestroyUi(player, OverlayName); CuiHelper.DestroyUi(player, MainPanelName); CuiHelper.DestroyUi(player, ScrollContainerName); CuiHelper.DestroyUi(player, ScrollViewName); CuiHelper.DestroyUi(player, AmountPanelName); CuiHelper.DestroyUi(player, SpawnPanelName); for (int i = 0; i < LootContainers.Length; i++) CuiHelper.DestroyUi(player, "TestLoot_Row_" + i); } private void OpenTestLootUi(BasePlayer player) { if (player == null || !player.IsConnected) return; var container = new CuiElementContainer(); string overlay = CreatePanel(ref container, "0 0", "1 1", "0.2 0.2 0.2 0.6", "Overlay", OverlayName, true, true); container.Add(new CuiButton { Button = { Color = "0 0 0 0", Command = ConsoleCommandPrefix + ".close" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }, Text = { Text = "" } }, overlay); string main = CreatePanel(ref container, "0.15 0.08", "0.85 0.92", "0.15 0.15 0.18 0.98", overlay, MainPanelName, true, false); container.Add(new CuiLabel { Text = { Color = "1 1 1 1", Text = "Test Loot", FontSize = 20, Align = TextAnchor.MiddleCenter }, RectTransform = { AnchorMin = "0 0.94", AnchorMax = "1 0.99" } }, main); container.Add(new CuiButton { Button = { Color = "0.6 0.2 0.2 1", Command = ConsoleCommandPrefix + ".close" }, RectTransform = { AnchorMin = "0.88 0.94", AnchorMax = "0.98 0.99" }, Text = { Text = "Close", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } }, main); string scrollContainer = CreatePanel(ref container, "0.02 0.38", "0.98 0.92", "0.1 0.1 0.12 0.95", main, ScrollContainerName); int gridRows = (LootContainers.Length + GridColumns - 1) / GridColumns; int contentHeightPx = gridRows * (RowHeightPx + RowSpacingPx); int panelSize = -contentHeightPx; container.Add(new CuiElement { Name = ScrollViewName, Parent = ScrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0.12 0.12 0.14 0.95" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = ScrollRect.MovementType.Elastic, Elasticity = 0.1f, Inertia = true, DecelerationRate = 0.135f, ScrollSensitivity = 20f, ContentTransform = new CuiRectTransform { AnchorMin = "0 0.98", AnchorMax = "1 0.98", OffsetMin = "0 " + panelSize, OffsetMax = "0 0", Pivot = "0.5 1" } }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", Pivot = "0.5 1" } } }); float totalGaps = GridColumnGap * (GridColumns - 1); float colWidth = (0.96f - totalGaps) / GridColumns; for (int i = 0; i < LootContainers.Length; i++) { int col = i % GridColumns; int row = i / GridColumns; float anchorMinX = 0.02f + col * (colWidth + GridColumnGap); float anchorMaxX = 0.02f + col * (colWidth + GridColumnGap) + colWidth; int rowOffset = row * (RowHeightPx + RowSpacingPx); int offsetMin = -rowOffset - RowHeightPx; int offsetMax = -rowOffset; string rowName = "TestLoot_Row_" + i; container.Add(new CuiElement { Name = rowName, Parent = ScrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = anchorMinX + " 0.998", AnchorMax = anchorMaxX + " 0.998", OffsetMin = "0 " + offsetMin, OffsetMax = "0 " + offsetMax }, new CuiImageComponent { Color = "0.2 0.22 0.26 0.9" } } }); container.Add(new CuiButton { Button = { Color = "0.25 0.28 0.32 0.95", Command = ConsoleCommandPrefix + ".select " + i }, RectTransform = { AnchorMin = GridCellPadding + " " + GridCellPadding, AnchorMax = (1f - GridCellPadding) + " " + (1f - GridCellPadding) }, Text = { Text = LootContainers[i], FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } }, rowName); } string amountPanel = CreatePanel(ref container, "0.02 0.28", "0.98 0.36", "0.1 0.1 0.12 0.95", main, AmountPanelName); container.Add(new CuiLabel { Text = { Color = "0.9 0.9 0.9 1", Text = "Amount", FontSize = 12, Align = TextAnchor.MiddleLeft }, RectTransform = { AnchorMin = "0 0.5", AnchorMax = "0.15 1" } }, amountPanel); int[] amounts = { 1, 5, 10, 25, 50, 100, 200 }; for (int a = 0; a < amounts.Length; a++) { float ax = 0.18f + a * 0.115f; container.Add(new CuiButton { Button = { Color = "0.3 0.35 0.4 1", Command = ConsoleCommandPrefix + ".amount " + amounts[a] }, RectTransform = { AnchorMin = ax + " 0.1", AnchorMax = (ax + 0.11f) + " 0.9" }, Text = { Text = amounts[a].ToString(), FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } }, amountPanel); } string spawnPanel = CreatePanel(ref container, "0.02 0.08", "0.98 0.26", "0.1 0.1 0.12 0.95", main, SpawnPanelName); container.Add(new CuiButton { Button = { Color = "0.2 0.5 0.25 1", Command = ConsoleCommandPrefix + ".spawn" }, RectTransform = { AnchorMin = "0.2 0.2", AnchorMax = "0.8 0.8" }, Text = { Text = "Spawn", FontSize = 16, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } }, spawnPanel); CuiHelper.AddUi(player, container); } private static string CreatePanel(ref CuiElementContainer container, string anchorMin, string anchorMax, string panelColor, string parent, string panelName = null, bool cursor = false, bool blur = false) { var panel = new CuiPanel { RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }, Image = { Color = panelColor, FadeIn = 0f }, CursorEnabled = cursor }; if (blur) panel.Image.Material = "assets/content/ui/uibackgroundblur.mat"; return container.Add(panel, parent, panelName); } } }