using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.IO; using Oxide.Core; using Oxide.Core.Plugins; using Oxide.Game.Rust.Cui; using UnityEngine; using Newtonsoft.Json; using HarmonyLib; namespace Oxide.Plugins { [Info("CupboardPlus", "Uzumi", "1.1.4")] [Description("Allows players to upgrade and customize their base via the tool cupboard.")] public class CupboardPlus : RustPlugin { #region Fields private const string PermissionUse = "cupboardplus.use"; private const string PermissionUpgrade = "cupboardplus.upgrade"; private const string PermissionSkin = "cupboardplus.skin"; private const string PermissionRepair = "cupboardplus.repair"; private const string PermissionWallpaper = "cupboardplus.wallpaper"; private const string PermissionDowngrade = "cupboardplus.downgrade"; private const string PermissionNoCost = "cupboardplus.nocost"; private const string PermissionAdmin = "cupboardplus.admin"; private const string GUIPanelName = "UICupboardPlus"; private static CupboardPlus PluginInstance; [PluginReference] private Plugin NoEscape; [PluginReference] private Plugin PlayerDLCAPI; [PluginReference] private Plugin ImageLibrary; private Configuration _config; private Dictionary> _costCache = new Dictionary>(); private Harmony _harmony; private readonly HashSet _uiViewers = new HashSet(); private readonly Dictionary _activeQueues = new Dictionary(); private readonly Dictionary _activeSkinQueues = new Dictionary(); private readonly Dictionary _activeColorQueues = new Dictionary(); private readonly Dictionary _activeRepairQueues = new Dictionary(); private readonly Dictionary _activeDowngradeQueues = new Dictionary(); private readonly Dictionary _activeWallpaperQueues = new Dictionary(); private const int ClothItemId = -858312878; private const int WallpaperClothCost = 5; private readonly Dictionary _selectedTiers = new Dictionary(); private readonly Dictionary _skinSelectedTier = new Dictionary(); private readonly Dictionary _openPanels = new Dictionary(); private readonly Dictionary> _ownedSkinCache = new Dictionary>(); private readonly Dictionary> _ownedWallpaperCache = new Dictionary>(); private readonly Dictionary> _ownedDlcPacksCache = new Dictionary>(); private List _cachedWallpapers = null; private Dictionary> _cachedWallpapersByType = null; private readonly Dictionary _skinUpgradeEffectEnabled = new Dictionary(); private readonly Dictionary _selectedWallpaperTab = new Dictionary(); private readonly Dictionary _selectedWallpaperTier = new Dictionary(); private readonly Dictionary _settingsPanelOpen = new Dictionary(); private readonly Dictionary _selectedColorIndex = new Dictionary(); private readonly Dictionary _randomColorIndex = new Dictionary(); private readonly Dictionary _randomColorTimers = new Dictionary(); private readonly Dictionary _activeToolCupboards = new Dictionary(); private readonly Dictionary _wallpaperInternalCache = new Dictionary(); private readonly Dictionary _pendingWallpaperFromTool = new Dictionary(); private readonly Dictionary> _skinPanelContentIds = new Dictionary>(); private readonly Dictionary _blockBuilderCache = new Dictionary(); private UISettings _uiSettings; #endregion #region Configuration private class Configuration { [JsonProperty("Upgrade interval seconds")] public float UpgradeIntervalSeconds = 1.0f; [JsonProperty("Enable upgrade animation")] public bool EnableUpgradeAnimation = true; [JsonProperty("Enable downgrade")] public bool EnableDowngrade = false; [JsonProperty("UI Settings")] public UISettings UISettings = new UISettings(); [JsonProperty("Header Image URL")] public string HeaderImageUrl = "https://cdn3.mapstr.gg/030480909803e47414b124bbc8d91435.png"; [JsonProperty("Hammer Image URL")] public string HammerImageUrl = "https://cdn3.mapstr.gg/61b497c73fd2c1544554b2e8e2c0a3e4.png"; [JsonProperty("Spray Can Image URL")] public string SprayCanImageUrl = "https://cdn3.mapstr.gg/70e5648f8096313cc375738ae6b7bc4f.png"; [JsonProperty("Repair Image URL")] public string RepairImageUrl = "https://cdn3.mapstr.gg/0945991cf5a29662edf460c575846cd0.png"; [JsonProperty("Wallpaper Image URL")] public string WallpaperImageUrl = "https://cdn3.mapstr.gg/2285dc5c650f1c63eee99b2fb1839106.png"; [JsonProperty("Settings Icon URL")] public string SettingsIconUrl = "https://cdn3.mapstr.gg/c9965709071f2caafc637324074bb71b.png"; [JsonProperty("Upgrade Button Image URL")] public string UpgradeButtonImageUrl = "https://cdn3.mapstr.gg/7d89978e143051ea48e6cf55b19472ca.png"; [JsonProperty("Skin Button Image URL")] public string SkinButtonImageUrl = "https://cdn3.mapstr.gg/43020936b1625ecdc491f6a08fcf4208.png"; [JsonProperty("Skin Image URLs")] public Dictionary SkinImageUrls = new Dictionary { ["Wood"] = "https://cdn3.mapstr.gg/495d9eea48104d5435c572622560b066.png", ["Legacy Wood"] = "https://cdn3.mapstr.gg/e4900e505959b45d817f9fbe5829f7fa.png", ["Stone"] = "https://cdn3.mapstr.gg/dbdeae837e3d18e9621ca7314f26035b.png", ["Adobe"] = "https://cdn3.mapstr.gg/8bc2c2d23657ccadaa0259b9b3874ee9.png", ["Brick"] = "https://cdn3.mapstr.gg/65edfac27afdbd04c9ffb827fd84194e.png", ["Brutalist"] = "https://cdn3.mapstr.gg/299cf51df54cd14b7205aa3bbd04869c.png", ["Jungle"] = "https://cdn3.mapstr.gg/f4b009d2f4ebb7b1ebc2fee02e03faf0.png", ["Metal"] = "https://cdn3.mapstr.gg/d847c52a391a5d3bdf360f26fc002b3d.png", ["Container"] = "https://cdn3.mapstr.gg/0c50e5a82d0c5d16ecf3f631796074cc.png", ["Armored"] = "https://cdn3.mapstr.gg/0a532d3c7072e79b77af7ed7d861809e.png", ["Space Station"] = "https://cdn3.mapstr.gg/2ec32349c1cb62b9816a4f90d288039d.png" }; [JsonProperty("Default Skin Checkmark Image URL")] public string DefaultSkinCheckmarkImageUrl = "https://cdn3.mapstr.gg/cc9c79fc4939d214dba1658f333264a7.png"; [JsonProperty("Wallpaper None Image URL")] public string WallpaperNoneImageUrl = "https://cdn3.mapstr.gg/e2884cf43b0e7752fe691ed16bb15762.png"; [JsonProperty("Skin Preview Placeholder Image URL")] public string SkinPreviewPlaceholderImageUrl = "https://cdn3.mapstr.gg/25ee457984b36c1a238c693c2dc2c030.png"; } private class UISettings { [JsonProperty("Background Color")] public string BackgroundColor = "0 0 0 0.6"; [JsonProperty("UI Placement (1-3)")] public int UIPlacement = 1; } private class PlayerData { [JsonProperty("SelectedColorIndex")] public int SelectedColorIndex = -1; [JsonProperty("LastUsedSkin")] public Dictionary LastUsedSkin = new Dictionary(); [JsonProperty("DefaultSkinPerTier")] public Dictionary DefaultSkinPerTier = new Dictionary(); [JsonProperty("UpgradeEffectEnabled")] public bool UpgradeEffectEnabled = false; [JsonProperty("WallpaperAdvancedMode")] public bool WallpaperAdvancedMode = false; } private Dictionary _playerData = new Dictionary(); private static string[] ColorPalette = new string[] { "0.5 0.5 0.5", "0.376 0.557 0.741", "0.451 0.714 0.345", "0.569 0.286 0.831", "0.416 0.165 0.110", "0.816 0.459 0.133", "0.867 0.867 0.867", "0.196 0.196 0.180", "0.400 0.333 0.278", "0.196 0.220 0.333", "0.239 0.345 0.196", "0.725 0.294 0.180", "0.776 0.529 0.388", "0.839 0.663 0.220", "0.337 0.325 0.310", "0.212 0.337 0.373", "0.659 0.608 0.565" }; protected override void LoadDefaultConfig() { _config = new Configuration(); SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new JsonException(); } catch { LoadDefaultConfig(); } _uiSettings = _config.UISettings; if (_uiSettings.UIPlacement < 1 || _uiSettings.UIPlacement > 3) { _uiSettings.UIPlacement = 1; SaveConfig(); } } protected override void SaveConfig() => Config.WriteObject(_config, true); #endregion #region Language protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["UI.Upgrade"] = "UPGRADE", ["UI.Downgrade"] = "DOWNGRADE", ["UI.Repair"] = "REPAIR", ["UI.Skin"] = "SKIN", ["UI.Wallpaper"] = "WALLPAPER", ["UI.AuthedUsers"] = "Authorized Players", ["UI.Settings"] = "SETTINGS", ["UI.Close"] = "CLOSE", ["UI.Stop"] = "STOP", ["UI.ColorPalette"] = "COLOR PALETTE", ["UI.InternalWallpaperOn"] = "INTERNAL: ON", ["UI.InternalWallpaperOff"] = "INTERNAL: OFF", ["UI.Foundation"] = "FOUNDATION", ["UI.Wall"] = "WALL", ["UI.Ceiling"] = "CEILING", ["UI.Free"] = "FREE", ["UI.Upgrading"] = "UPGRADING", ["UI.Downgrading"] = "DOWNGRADING", ["UI.Repairing"] = "REPAIRING", ["UI.Skinning"] = "SKINNING", ["UI.ApplyingWallpaper"] = "APPLYING WALLPAPER", ["Error.NoPermission"] = "You don't have permission to use this command.", ["Error.NoTC"] = "You must be near a tool cupboard to use this feature.", ["Error.NoTCUpgrade"] = "You must be authorized on a Tool Cupboard to upgrade.", ["Error.NoTCSkin"] = "You must be authorized on a Tool Cupboard to use skins.", ["Error.NoTCRepair"] = "You must be authorized on a Tool Cupboard to repair.", ["Error.NoTCDowngrade"] = "You must be authorized on a Tool Cupboard to downgrade.", ["Error.NoTCWallpaper"] = "You must be authorized on a Tool Cupboard to use wallpapers.", ["Error.NoResources"] = "Not enough resources in the tool cupboard.", ["Error.NoBlocks"] = "No building blocks connected to this Tool Cupboard.", ["Error.NoDamagedBlocks"] = "No damaged blocks found.", ["Error.UpgradeStopped"] = "Upgrade stopped due to lack of resources.", ["Error.DowngradeStopped"] = "Downgrade stopped due to lack of resources.", ["Error.RepairStopped"] = "Repair stopped due to lack of resources.", ["Error.WallpaperStopped"] = "Wallpaper application stopped due to lack of resources.", ["Error.SkinStopped"] = "Skin application stopped due to lack of resources.", ["Error.SkinOwnershipUnavailable"] = "Skin ownership checking is not available. Please try again in a moment.", ["Error.SkinOwnershipError"] = "Error checking skin ownership. Please try again.", ["Error.SkinOwnershipAdmin"] = "Skin ownership checking is not available. Please contact an administrator.", ["Error.SkinNotOwned"] = "You do not own this skin. Purchase it from the Steam Workshop to use it.", ["Error.WallpaperOwnershipUnavailable"] = "Wallpaper ownership checking is not available. Please try again in a moment.", ["Error.WallpaperOwnershipError"] = "Error checking wallpaper ownership. Please try again.", ["Error.WallpaperOwnershipAdmin"] = "Wallpaper ownership checking is not available. Please contact an administrator.", ["Error.WallpaperNotOwned"] = "You do not own this wallpaper. Purchase it from the Steam Workshop to use it.", ["Error.DowngradeDisabled"] = "Downgrade is disabled.", ["Error.InvalidTier"] = "Invalid tier specified.", ["Error.NoTierSelected"] = "Please select a tier first.", ["Error.NoBlocksToDowngrade"] = "No blocks can be downgraded to this tier.", ["Error.WallpaperCommandInvalid"] = "Invalid wallpaper command format.", ["Error.WallpaperIdInvalid"] = "Invalid wallpaper ID format.", ["Error.UnderAttack"] = "Operation interrupted: Base is under attack.", ["Success.UpgradeComplete"] = "Upgrade process completed.", ["Success.DowngradeComplete"] = "Downgrade process completed.", ["Success.RepairComplete"] = "Repair process completed.", ["Success.WallpaperComplete"] = "Wallpaper application completed.", ["Success.SkinComplete"] = "Skin application completed.", ["Success.AllBlocksHaveSkin"] = "All {0} blocks already have {1} skin.", ["Command.Bu.Help"] = "CupboardPlus Commands:\n/cplus speed - Set upgrade interval\n/cplus downgrade - Toggle downgrade feature\nCurrent settings:\n- Upgrade interval: {0} seconds\n- Downgrade enabled: {1}", ["Command.Bu.SpeedSet"] = "Upgrade interval set to {0} seconds.", ["Command.Bu.SpeedInvalid"] = "Invalid speed value. Use a number between 0.1 and 10.", ["Command.Bu.DowngradeToggled"] = "Downgrade feature {0}.", ["Command.Bu.NoArgs"] = "Type /cplus for command help.", ["Wallpaper.AlreadyApplied"] = "All selected blocks already have this wallpaper applied.", ["Wallpaper.NoneSelected"] = "No wallpaper selected.", ["Wallpaper.AppliedCount"] = "Applied {0} to {1} {2} block(s).", ["Wallpaper.CompletedCount"] = "Wallpaper application interrupted: Base is under attack. Completed {0}/{1} blocks.", ["Error.UnderAttack"] = "Cannot upgrade while base is under attack.", ["Error.UpgradeInterrupted"] = "Upgrade interrupted: Base is under attack. Completed {0}/{1} blocks.", ["Error.UpgradePaused"] = "Upgrade paused: Not enough resources. Completed {0}/{1} blocks. Add resources to continue.", ["Error.RepairInterrupted"] = "Repair interrupted: Base is under attack. Completed {0}/{1} blocks.", ["Error.DowngradeInterrupted"] = "Downgrade interrupted: Base is under attack. Completed {0}/{1} blocks.", ["Error.DowngradePaused"] = "Downgrade paused: Not enough resources. Completed {0}/{1} blocks. Add resources to continue.", ["Error.WallpaperPaused"] = "Wallpaper application paused: Not enough cloth (need {0}, have {1}). Completed {2}/{3} blocks. Add cloth to continue.", ["Error.SkinInterrupted"] = "Skin application interrupted: Base is under attack. Completed {0}/{1} blocks.", ["Error.ColorInterrupted"] = "Color change interrupted: Base is under attack. Completed {0}/{1} pieces.", ["Command.Bu.SpeedInvalidRange"] = "Invalid speed. Use a number >= 0 (0 = instant).", ["Command.Bu.SpeedSetTo"] = "Speed set to: {0}", ["Command.Bu.HelpHeader"] = "=== CupboardPlus Admin Commands ===", ["Command.Bu.AvailableCommands"] = "Available Commands:", ["Command.Bu.SpeedHelp"] = " /cplus speed - Set upgrade speed", ["Command.Bu.SpeedExamples"] = " • 0 = instant | 1 = 1 per second | 2 = 1 per 2 seconds", ["Command.Bu.DowngradeHelp"] = " /cplus downgrade - Toggle downgrade feature", ["Command.Bu.UIPlacementHelp"] = " /cplus ui <1-3> - Set UI placement", ["Command.Bu.CurrentSettings"] = "Current Settings:", ["Command.Bu.UpgradeSpeed"] = " • Upgrade Speed: {0}", ["Command.Bu.UpgradeAnimation"] = " • Upgrade Animation: {0}", ["Command.Bu.DowngradeStatus"] = " • Downgrade: {0}", ["UI.Enabled"] = "ENABLED", ["UI.Disabled"] = "DISABLED", ["Wallpaper.Applying"] = "Applying wallpaper to {0} blocks...", ["Wallpaper.ApplyingWithSpeed"] = "Applying wallpaper to {0} blocks..." }, this); } private string Lang(string key, string userId = null, params object[] args) { string message = lang.GetMessage(key, this, userId); return args.Length > 0 ? string.Format(message, args) : message; } #endregion #region Initialization private void Init() { PluginInstance = this; if (ColorPalette == null) { ColorPalette = new string[] { "0.5 0.5 0.5", "0.376 0.557 0.741", "0.451 0.714 0.345", "0.569 0.286 0.831", "0.416 0.165 0.110", "0.816 0.459 0.133", "0.867 0.867 0.867", "0.196 0.196 0.180", "0.400 0.333 0.278", "0.196 0.220 0.333", "0.239 0.345 0.196", "0.725 0.294 0.180", "0.776 0.529 0.388", "0.839 0.663 0.220", "0.337 0.325 0.310", "0.212 0.337 0.373", "0.659 0.608 0.565" }; } permission.RegisterPermission(PermissionUse, this); permission.RegisterPermission(PermissionUpgrade, this); permission.RegisterPermission(PermissionSkin, this); permission.RegisterPermission(PermissionRepair, this); permission.RegisterPermission(PermissionWallpaper, this); permission.RegisterPermission(PermissionDowngrade, this); permission.RegisterPermission(PermissionNoCost, this); permission.RegisterPermission(PermissionAdmin, this); Subscribe("OnLootEntity"); Subscribe("OnPlayerLootEnd"); Subscribe("OnPlayerDisconnected"); Subscribe("OnPayForPlacement"); Subscribe("OnWallpaperSet"); Subscribe("OnEntityBuilt"); Subscribe("OnEntityKill"); _uiSettings = _config.UISettings; LoadData(); InitializeHarmonyPatches(); } private void LoadData() { _playerData = Interface.Oxide.DataFileSystem.ReadObject>(Name); if (_playerData == null) { _playerData = new Dictionary(); } } private void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(Name, _playerData); } private PlayerData GetPlayerData(ulong userId) { if (!_playerData.TryGetValue(userId, out var data)) { data = new PlayerData(); _playerData[userId] = data; } return data; } private void OnServerInitialized() { CacheAllWallpapers(); if (ImageLibrary != null) { RegisterSkinImages(); } else { } } private void RegisterSkinImages() { const string headerImageName = "CupboardPlusHeader"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.HeaderImageUrl)) { ImageLibrary.Call("AddImage", _config.HeaderImageUrl, headerImageName); } const string hammerImageName = "CupboardPlusHammer"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.HammerImageUrl)) { ImageLibrary.Call("AddImage", _config.HammerImageUrl, hammerImageName); } const string sprayCanImageName = "CupboardPlusSprayCan"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.SprayCanImageUrl)) { ImageLibrary.Call("AddImage", _config.SprayCanImageUrl, sprayCanImageName); } const string repairImageName = "CupboardPlusRepair"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.RepairImageUrl)) { ImageLibrary.Call("AddImage", _config.RepairImageUrl, repairImageName); } const string wallpaperImageName = "CupboardPlusWallpaper"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.WallpaperImageUrl)) { ImageLibrary.Call("AddImage", _config.WallpaperImageUrl, wallpaperImageName); } const string settingsIconName = "CupboardPlusSettings"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.SettingsIconUrl)) { ImageLibrary.Call("AddImage", _config.SettingsIconUrl, settingsIconName); } const string wallpaperNoneImageName = "CupboardPlusWallpaperNone"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.WallpaperNoneImageUrl)) { ImageLibrary.Call("AddImage", _config.WallpaperNoneImageUrl, wallpaperNoneImageName); } const string skinButtonImageName = "CupboardPlusSkinButton"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.SkinButtonImageUrl)) { ImageLibrary.Call("AddImage", _config.SkinButtonImageUrl, skinButtonImageName); } const string upgradeButtonImageName = "CupboardPlusUpgradeButton"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.UpgradeButtonImageUrl)) { ImageLibrary.Call("AddImage", _config.UpgradeButtonImageUrl, upgradeButtonImageName); } const string defaultCheckmarkImageName = "CupboardPlusDefaultCheck"; if (ImageLibrary != null && !string.IsNullOrEmpty(_config.DefaultSkinCheckmarkImageUrl)) { ImageLibrary.Call("AddImage", _config.DefaultSkinCheckmarkImageUrl, defaultCheckmarkImageName); } foreach (var kvp in _config.SkinImageUrls) { if (!string.IsNullOrEmpty(kvp.Value)) { ImageLibrary.Call("AddImage", kvp.Value, kvp.Key); } } } private void Unload() { _harmony?.UnpatchAll("com.uzumi.cupboardplus"); foreach (var player in BasePlayer.activePlayerList) { if (player != null && player.IsConnected) { CuiHelper.DestroyUi(player, GUIPanelName); CuiHelper.DestroyUi(player, "UISkin"); CuiHelper.DestroyUi(player, "UIUpgrade"); CuiHelper.DestroyUi(player, "UIRepair"); CuiHelper.DestroyUi(player, "UIWallpaper"); CuiHelper.DestroyUi(player, "UIAuthedUsers"); CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "RepairMainPanel"); CuiHelper.DestroyUi(player, "RepairContentPanel"); CuiHelper.DestroyUi(player, "WallpaperMainPanel"); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); CuiHelper.DestroyUi(player, "SkinPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); } } foreach (var queue in _activeQueues.Values) { if (queue != null) queue.Stop(); } foreach (var queue in _activeSkinQueues.Values) { if (queue != null) queue.Stop(); } foreach (var queue in _activeColorQueues.Values) { if (queue != null) queue.Stop(); } foreach (var queue in _activeRepairQueues.Values) { if (queue != null) queue.Stop(); } foreach (var queue in _activeDowngradeQueues.Values) { if (queue != null) queue.Stop(); } foreach (var queue in _activeWallpaperQueues.Values) { if (queue != null) queue.Stop(); } _activeQueues.Clear(); _activeSkinQueues.Clear(); _activeColorQueues.Clear(); _activeRepairQueues.Clear(); _activeDowngradeQueues.Clear(); _activeWallpaperQueues.Clear(); _uiViewers.Clear(); _selectedTiers.Clear(); _skinSelectedTier.Clear(); _openPanels.Clear(); _ownedSkinCache.Clear(); _ownedWallpaperCache.Clear(); _settingsPanelOpen.Clear(); _selectedColorIndex.Clear(); _costCache.Clear(); _activeToolCupboards.Clear(); foreach (var timer in _randomColorTimers.Values) { timer?.Destroy(); } _randomColorTimers.Clear(); _randomColorIndex.Clear(); _wallpaperInternalCache.Clear(); PluginInstance = null; ColorPalette = null; SaveData(); } #endregion #region Hooks private void OnLootEntity(BasePlayer basePlayer, BaseEntity entity) { if (basePlayer == null || entity == null || !(entity is BuildingPrivlidge)) return; if (!HasPermission(basePlayer)) return; var tc = entity as BuildingPrivlidge; if (tc != null) { _activeToolCupboards[basePlayer.userID] = tc; } timer.Once(0.1f, () => { if (basePlayer == null || entity == null || entity.IsDestroyed) return; CreateButtonUI(basePlayer); }); } private void OnPlayerLootEnd(PlayerLoot inventory) { var player = inventory.baseEntity; if (player != null) { CuiHelper.DestroyUi(player, GUIPanelName); CuiHelper.DestroyUi(player, "UISkin"); CuiHelper.DestroyUi(player, "UIUpgrade"); CuiHelper.DestroyUi(player, "UIRepair"); CuiHelper.DestroyUi(player, "UIWallpaper"); CuiHelper.DestroyUi(player, "UIAuthedUsers"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _uiViewers.Remove(player.userID); _activeToolCupboards.Remove(player.userID); CloseUpgradeUI(player); CloseSkinUI(player); CloseRepairUI(player); CloseWallpaperUI(player); CloseAuthedUsersUI(player); } } private void OnLootEntityEnd(BasePlayer player, BaseCombatEntity entity) { if (entity is BuildingPrivlidge) { CuiHelper.DestroyUi(player, GUIPanelName); CuiHelper.DestroyUi(player, "UISkin"); CuiHelper.DestroyUi(player, "UIUpgrade"); CuiHelper.DestroyUi(player, "UIRepair"); CuiHelper.DestroyUi(player, "UIWallpaper"); CuiHelper.DestroyUi(player, "UIAuthedUsers"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _uiViewers.Remove(player.userID); _activeToolCupboards.Remove(player.userID); CloseUpgradeUI(player); CloseSkinUI(player); CloseRepairUI(player); CloseWallpaperUI(player); CloseAuthedUsersUI(player); } } private void OnEntityBuilt(Planner plan, GameObject gameObject) { if (plan == null || gameObject == null) return; var player = plan.GetOwnerPlayer(); if (player == null || player.userID == 0) return; var buildingBlock = gameObject.GetComponent(); if (buildingBlock == null) return; ulong netId = buildingBlock.net?.ID.Value ?? 0UL; if (netId != 0) _blockBuilderCache[netId] = player.userID; } private void OnEntityKill(BaseEntity entity) { if (entity == null) return; if (entity is BuildingBlock) { ulong netId = entity.net?.ID.Value ?? 0UL; if (netId != 0) _blockBuilderCache.Remove(netId); } } private void OnPlayerDisconnected(BasePlayer player) { if (player == null) return; ulong userId = player.userID; if (_activeQueues.TryGetValue(userId, out var upgradeQueue)) { upgradeQueue.Stop(); _activeQueues.Remove(userId); } if (_activeSkinQueues.TryGetValue(userId, out var skinQueue)) { skinQueue.Stop(); _activeSkinQueues.Remove(userId); } if (_activeColorQueues.TryGetValue(userId, out var colorQueue)) { colorQueue.Stop(); _activeColorQueues.Remove(userId); } if (_activeRepairQueues.TryGetValue(userId, out var repairQueue)) { repairQueue.Stop(); _activeRepairQueues.Remove(userId); } if (_activeDowngradeQueues.TryGetValue(userId, out var downgradeQueue)) { downgradeQueue.Stop(); _activeDowngradeQueues.Remove(userId); } if (_activeWallpaperQueues.TryGetValue(userId, out var wallpaperQueue)) { wallpaperQueue.Stop(); _activeWallpaperQueues.Remove(userId); } SaveData(); } #endregion #region UI Methods private void CreateButtonUI(BasePlayer player) { if (!_uiViewers.Add(player.userID)) return; if (!HasPermission(player)) return; bool hasUpgradePermission = HasUpgradePermission(player); bool hasSkinPermission = HasSkinPermission(player); bool hasRepairPermission = HasRepairPermission(player); bool hasWallpaperPermission = HasWallpaperPermission(player); bool playerDLCAPIAvailable = false; if (PlayerDLCAPI != null) { try { object initializedResult = PlayerDLCAPI.Call("Initialized"); playerDLCAPIAvailable = initializedResult is bool && (bool)initializedResult; } catch { playerDLCAPIAvailable = false; } } if (hasSkinPermission && !playerDLCAPIAvailable) { hasSkinPermission = false; } if (hasWallpaperPermission && !playerDLCAPIAvailable) { hasWallpaperPermission = false; } if (!hasUpgradePermission && !hasSkinPermission && !hasRepairPermission && !hasWallpaperPermission) { return; } var elements = new CuiElementContainer(); int uiPlacement = _uiSettings.UIPlacement; bool showIcons = uiPlacement == 2; bool useTopRight = uiPlacement == 3; float upgradeXMin = 0f, upgradeYMin = 0f, upgradeXMax = 0f, upgradeYMax = 0f; float repairXMin = 0f, repairYMin = 0f, repairXMax = 0f, repairYMax = 0f; float wallpaperXMin = 0f, wallpaperYMin = 0f, wallpaperXMax = 0f, wallpaperYMax = 0f; float skinXMin = 0f, skinYMin = 0f, skinXMax = 0f, skinYMax = 0f; if (useTopRight) { float startX = 0.78f; float startY = 0.93f; float buttonWidth = 0.08f; float buttonHeight = 0.03f; float spacing = 0.005f; float topRowY = startY; float bottomRowY = startY - buttonHeight - spacing; float leftColX = startX; float rightColX = startX + buttonWidth + spacing; if (hasUpgradePermission) { upgradeXMin = leftColX; upgradeYMin = bottomRowY - buttonHeight; upgradeXMax = leftColX + buttonWidth; upgradeYMax = bottomRowY; } if (hasRepairPermission) { repairXMin = rightColX; repairYMin = bottomRowY - buttonHeight; repairXMax = rightColX + buttonWidth; repairYMax = bottomRowY; } if (hasWallpaperPermission) { wallpaperXMin = leftColX; wallpaperYMin = topRowY - buttonHeight; wallpaperXMax = leftColX + buttonWidth; wallpaperYMax = topRowY; } if (hasSkinPermission) { skinXMin = rightColX; skinYMin = topRowY - buttonHeight; skinXMax = rightColX + buttonWidth; skinYMax = topRowY; } } else if (!showIcons) { const float startX = 0.655f; const float startY = 0.135f; const float buttonWidth = 0.0854f; const float buttonHeight = 0.035f; const float spacing = 0.005f; float topRowY = startY; float bottomRowY = startY - buttonHeight - spacing; float leftColX = startX; float rightColX = startX + buttonWidth + spacing; if (hasUpgradePermission) { upgradeXMin = leftColX; upgradeYMin = bottomRowY - buttonHeight; upgradeXMax = leftColX + buttonWidth; upgradeYMax = bottomRowY; } if (hasRepairPermission) { repairXMin = rightColX; repairYMin = bottomRowY - buttonHeight; repairXMax = rightColX + buttonWidth; repairYMax = bottomRowY; } if (hasWallpaperPermission) { wallpaperXMin = leftColX; wallpaperYMin = topRowY - buttonHeight; wallpaperXMax = leftColX + buttonWidth; wallpaperYMax = topRowY; } if (hasSkinPermission) { skinXMin = rightColX; skinYMin = topRowY - buttonHeight; skinXMax = rightColX + buttonWidth; skinYMax = topRowY; } } else { const float buttonYMin = 0.0225f; const float buttonYMax = 0.135f; const float buttonHeight = buttonYMax - buttonYMin; const float smallButtonHeight = buttonHeight * 0.49f; const float gap = 0.01f; const float smallButtonWidth = 0.03f; const float centerY = (buttonYMin + buttonYMax) / 2f; const float gapBetweenButtons = 0.005f; const float totalWidth = 0.83f - 0.655f; int mainButtonCount = 0; if (hasUpgradePermission) mainButtonCount++; if (hasSkinPermission) mainButtonCount++; int smallButtonCount = 0; if (hasRepairPermission) smallButtonCount++; if (hasWallpaperPermission) smallButtonCount++; if (mainButtonCount == 0) { return; } float smallButtonsWidth = smallButtonCount > 0 ? (smallButtonWidth + gap) : 0f; float availableWidth = totalWidth - (gap * (mainButtonCount - 1)) - smallButtonsWidth; float mainButtonWidth = availableWidth / mainButtonCount; float currentX = 0.655f; if (hasUpgradePermission) { upgradeXMin = currentX; upgradeYMin = buttonYMin; upgradeXMax = currentX + mainButtonWidth; upgradeYMax = buttonYMax; currentX = upgradeXMax + gap; } if (smallButtonCount > 0) { float smallButtonX = currentX; if (hasRepairPermission) { repairXMin = smallButtonX; repairXMax = smallButtonX + smallButtonWidth; repairYMax = centerY - (gapBetweenButtons / 2f); repairYMin = repairYMax - smallButtonHeight; } if (hasWallpaperPermission) { wallpaperXMin = smallButtonX; wallpaperXMax = smallButtonX + smallButtonWidth; wallpaperYMin = centerY + (gapBetweenButtons / 2f); wallpaperYMax = wallpaperYMin + smallButtonHeight; } currentX = smallButtonX + smallButtonWidth + gap; } if (hasSkinPermission) { skinXMin = currentX; skinYMin = buttonYMin; skinXMax = 0.83f; skinYMax = buttonYMax; } } const string buttonColor = "1 1 1 0.15"; const string hammerImageName = "CupboardPlusHammer"; const string sprayCanImageName = "CupboardPlusSprayCan"; const string repairImageName = "CupboardPlusRepair"; const string wallpaperImageName = "CupboardPlusWallpaper"; const string upgradeButtonImageName = "CupboardPlusUpgradeButton"; const string skinButtonImageName = "CupboardPlusSkinButton"; string hammerImagePng = null; string sprayCanImagePng = null; string repairImagePng = null; string wallpaperImagePng = null; string upgradeButtonImagePng = null; string skinButtonImagePng = null; if (showIcons) { hammerImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", hammerImageName) : null; sprayCanImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", sprayCanImageName) : null; repairImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", repairImageName) : null; wallpaperImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", wallpaperImageName) : null; upgradeButtonImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", upgradeButtonImageName) : null; skinButtonImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", skinButtonImageName) : null; } if (hasUpgradePermission) { const string upgradePanelName = "UIUpgrade"; elements.Add(new CuiButton { Button = { Command = "cupboardplus.upgradeui", Color = buttonColor, }, RectTransform = { AnchorMin = $"{upgradeXMin} {upgradeYMin}", AnchorMax = $"{upgradeXMax} {upgradeYMax}", }, }, "Overlay", upgradePanelName); if (showIcons && !string.IsNullOrEmpty(upgradeButtonImagePng)) { elements.Add(new CuiElement { Parent = upgradePanelName, Components = { new CuiRawImageComponent { Png = upgradeButtonImagePng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } else { elements.Add(new CuiLabel { Text = { Color = "1 1 1 1", Text = "Upgrade", Align = TextAnchor.MiddleCenter, FontSize = 14, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, upgradePanelName); } } if (hasRepairPermission) { const string repairPanelName = "UIRepair"; elements.Add(new CuiButton { Button = { Command = "cupboardplus.repairui", Color = buttonColor, }, RectTransform = { AnchorMin = $"{repairXMin} {repairYMin}", AnchorMax = $"{repairXMax} {repairYMax}", }, }, "Overlay", repairPanelName); if (showIcons && !string.IsNullOrEmpty(repairImagePng)) { elements.Add(new CuiElement { Parent = repairPanelName, Components = { new CuiRawImageComponent { Png = repairImagePng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } else { elements.Add(new CuiLabel { Text = { Color = "1 1 1 1", Text = "Repair", Align = TextAnchor.MiddleCenter, FontSize = 14, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, repairPanelName); } } if (hasWallpaperPermission) { const string wallpaperPanelName = "UIWallpaper"; elements.Add(new CuiButton { Button = { Command = "cupboardplus.wallpaper", Color = buttonColor, }, RectTransform = { AnchorMin = $"{wallpaperXMin} {wallpaperYMin}", AnchorMax = $"{wallpaperXMax} {wallpaperYMax}", }, }, "Overlay", wallpaperPanelName); if (showIcons && !string.IsNullOrEmpty(wallpaperImagePng)) { elements.Add(new CuiElement { Parent = wallpaperPanelName, Components = { new CuiRawImageComponent { Png = wallpaperImagePng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } else { elements.Add(new CuiLabel { Text = { Color = "1 1 1 1", Text = "Wallpaper", Align = TextAnchor.MiddleCenter, FontSize = 14, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, wallpaperPanelName); } } if (hasSkinPermission) { const string skinPanelName = "UISkin"; elements.Add(new CuiButton { Button = { Command = "cupboardplus.skinui", Color = buttonColor, }, RectTransform = { AnchorMin = $"{skinXMin} {skinYMin}", AnchorMax = $"{skinXMax} {skinYMax}", }, }, "Overlay", skinPanelName); if (showIcons && !string.IsNullOrEmpty(skinButtonImagePng)) { elements.Add(new CuiElement { Parent = skinPanelName, Components = { new CuiRawImageComponent { Png = skinButtonImagePng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } else { elements.Add(new CuiLabel { Text = { Color = "1 1 1 1", Text = "Skin", Align = TextAnchor.MiddleCenter, FontSize = 14, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, skinPanelName); } } const float authedUsersButtonXMin = 0.85f; const float authedUsersButtonYMin = 0.828f; const float authedUsersButtonXMax = 0.947f; const float authedUsersButtonYMax = 0.855f; const string authedUsersPanelName = "UIAuthedUsers"; elements.Add(new CuiButton { Button = { Command = "cupboardplus.authedusersui", Color = buttonColor, }, RectTransform = { AnchorMin = $"{authedUsersButtonXMin} {authedUsersButtonYMin}", AnchorMax = $"{authedUsersButtonXMax} {authedUsersButtonYMax}", }, }, "Overlay", authedUsersPanelName); elements.Add(new CuiLabel { Text = { Color = "1 1 1 1", Text = Lang("UI.AuthedUsers", player.UserIDString), Align = TextAnchor.MiddleCenter, FontSize = 12, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, authedUsersPanelName); CuiHelper.AddUi(player, elements); } private void OpenUpgradeUI(BasePlayer player) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } CuiHelper.DestroyUi(player, "UpgradeMainPanel"); _openPanels[player.userID] = "UpgradeMainPanel"; var playerData = GetPlayerData(player.userID); if (playerData.SelectedColorIndex >= 0) { _selectedColorIndex[player.userID] = playerData.SelectedColorIndex; } _skinUpgradeEffectEnabled[player.userID] = playerData.UpgradeEffectEnabled; var blocks = GetConnectedBuildingBlocks(privilege, player); var availableTiers = GetAvailableUpgradeTiers(blocks); _selectedTiers[player.userID] = null; var elements = new CuiElementContainer(); var panel = CreatePanel(ref elements, "0.3435 0.121", "0.64 0.855", _uiSettings.BackgroundColor, "Overlay", "UpgradeMainPanel", true, true); const string headerImageName = "CupboardPlusHeader"; string headerImagePng = null; string headerImageUrl = null; if (ImageLibrary != null) { headerImagePng = (string)ImageLibrary.Call("GetImage", headerImageName); if (string.IsNullOrEmpty(headerImagePng)) { headerImageUrl = (string)ImageLibrary.Call("GetImage", headerImageName, 0, true); } } if (string.IsNullOrEmpty(headerImagePng) && string.IsNullOrEmpty(headerImageUrl)) { headerImageUrl = _config.HeaderImageUrl; } elements.Add(new CuiElement { Parent = panel, Name = "HeaderImage", Components = { new CuiRectTransformComponent { AnchorMin = "0.01 0.84", AnchorMax = "0.99 0.99" }, !string.IsNullOrEmpty(headerImagePng) ? new CuiRawImageComponent { Png = headerImagePng, Color = "1 1 1 1" } : new CuiRawImageComponent { Url = headerImageUrl, Color = "1 1 1 1" } } }); CreateTierTabs(ref elements, panel, availableTiers, player, null); string contentPanel = CreatePanel(ref elements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", panel, "ContentPanel"); CreateLabel(ref elements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "Select an upgrade tier above", 18, TextAnchor.MiddleCenter, contentPanel); const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closeui", 15); if (!_settingsPanelOpen.TryGetValue(player.userID, out var settingsOpen) || !settingsOpen) { CreateSettingsButton(ref elements, player); } CuiHelper.AddUi(player, elements); } private void OpenSkinUI(BasePlayer player) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } CuiHelper.DestroyUi(player, "SkinPanel"); _openPanels[player.userID] = "SkinPanel"; var playerData = GetPlayerData(player.userID); if (playerData.SelectedColorIndex >= 0) { _selectedColorIndex[player.userID] = playerData.SelectedColorIndex; } _skinUpgradeEffectEnabled[player.userID] = playerData.UpgradeEffectEnabled; CheckAndCacheOwnedSkins(player); var blocks = GetConnectedBuildingBlocks(privilege, player); if (blocks.Count == 0) { player.ChatMessage(Lang("Error.NoBlocks", player.UserIDString)); return; } var elements = new CuiElementContainer(); var panel = CreatePanel(ref elements, "0.3435 0.121", "0.64 0.855", _uiSettings.BackgroundColor, "Overlay", "SkinPanel", true, true); const string headerImageName = "CupboardPlusHeader"; string headerImagePng = null; string headerImageUrl = null; if (ImageLibrary != null) { headerImagePng = (string)ImageLibrary.Call("GetImage", headerImageName); if (string.IsNullOrEmpty(headerImagePng)) { headerImageUrl = (string)ImageLibrary.Call("GetImage", headerImageName, 0, true); } } if (string.IsNullOrEmpty(headerImagePng) && string.IsNullOrEmpty(headerImageUrl)) { headerImageUrl = _config.HeaderImageUrl; } elements.Add(new CuiElement { Parent = panel, Name = "HeaderImage", Components = { new CuiRectTransformComponent { AnchorMin = "0.01 0.84", AnchorMax = "0.99 0.99" }, !string.IsNullOrEmpty(headerImagePng) ? new CuiRawImageComponent { Png = headerImagePng, Color = "1 1 1 1" } : new CuiRawImageComponent { Url = headerImageUrl, Color = "1 1 1 1" } } }); var tiers = new[] { BuildingGrade.Enum.Wood, BuildingGrade.Enum.Stone, BuildingGrade.Enum.Metal, BuildingGrade.Enum.TopTier }; var tierNames = new[] { "WOOD", "STONE", "METAL", "ARMORED" }; float tabWidth = 0.23f; float startX = 0.02f; float yMin = 0.75f; float yMax = 0.8f; for (int i = 0; i < tiers.Length; i++) { var tier = tiers[i]; var tierName = tierNames[i]; float xMin = startX + (i * (tabWidth + 0.01f)); float xMax = xMin + tabWidth; const string buttonColor = "1 1 1 0.15"; string tabColor = buttonColor; string command = $"cupboardplus.selectskintab {tier}"; string buttonPanel = CreatePanel(ref elements, $"{xMin} {yMin}", $"{xMax} {yMax}", tabColor, panel, $"SkinTab_{tier}"); CreateLabel(ref elements, "0 0.1", "1 0.9", "1 1 1 1", "0 0 0 0", tierName, 14, TextAnchor.MiddleCenter, buttonPanel); elements.Add(new CuiButton { Button = { Color = "0 0 0 0", Command = command }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, buttonPanel, $"SkinTab_Button_{tier}"); } string contentPanel = CreatePanel(ref elements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", panel, "SkinContentPanel"); CreateLabel(ref elements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "Select a tier above", 18, TextAnchor.MiddleCenter, contentPanel); const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closeskin", 15); if (!_settingsPanelOpen.TryGetValue(player.userID, out var settingsOpen) || !settingsOpen) { CreateSettingsButton(ref elements, player); } CuiHelper.AddUi(player, elements); } private void RefreshSkinContentPanel(BasePlayer player, List blocks, BuildingGrade.Enum tier) { _skinSelectedTier[player.userID] = tier; var elements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "SkinContentPanel"); string contentPanel = CreatePanel(ref elements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "SkinPanel", "SkinContentPanel"); string tierName = GetTierName(tier); CreateLabel(ref elements, "0.05 0.85", "0.95 0.95", "1 1 1 1", "0 0 0 0", $"Select {tierName} Skin", 20, TextAnchor.MiddleCenter, contentPanel); var allSkins = GetAllTierSkins().Where(s => s.Tier == tier).ToList(); int defaultContentId = GetDefaultSkinContentId(player, tier); var ownedSkins = new List(); var defaultSkin = allSkins.FirstOrDefault(s => s.ContentId == defaultContentId); if (defaultSkin == null) defaultSkin = allSkins.FirstOrDefault(s => s.SkinName == "Default" || s.SkinName == tierName || s.SkinName == "Wood" || s.SkinName == "Stone" || s.SkinName == "Metal" || s.SkinName == "Armored"); if (defaultSkin != null) { ownedSkins.Add(defaultSkin); } else { ownedSkins.Add(new TierSkinInfo { Tier = tier, TierName = tierName, SkinName = "Default", ContentId = defaultContentId }); } if (_ownedSkinCache.TryGetValue(player.userID, out var ownedContentIds)) { foreach (var skin in allSkins) { if (skin.ContentId == defaultContentId) continue; if (defaultSkin != null && skin.ContentId == defaultSkin.ContentId) continue; if (ownedContentIds.Contains(skin.ContentId)) { ownedSkins.Add(skin); } } } var sortedSkins = ownedSkins.OrderBy(s => s.ContentId).ThenBy(s => s.SkinName).ToList(); string scrollContainerName = "SkinScrollContainer"; string scrollViewName = "SkinScrollView"; string scrollContainer = CreatePanel(ref elements, "0.05 0.13", "0.95 0.8", "0.12 0.12 0.12 0.3", contentPanel, scrollContainerName); int boxesPerRow = 2; int numberOfSkins = sortedSkins.Count; int numberOfRows = (int)Math.Ceiling(numberOfSkins / (double)boxesPerRow); int boxSpacing = 24; int boxHeightPx = 180; int panelSize = -boxSpacing * numberOfRows - boxHeightPx * numberOfRows; elements.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0.12 0.12 0.12 0.3" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = UnityEngine.UI.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" } } }); string sprayCanImagePng = null; if (tier == BuildingGrade.Enum.Metal && ImageLibrary != null) sprayCanImagePng = (string)ImageLibrary.Call("GetImage", "CupboardPlusSprayCan"); string defaultCheckmarkPng = null; if (ImageLibrary != null) defaultCheckmarkPng = (string)ImageLibrary.Call("GetImage", "CupboardPlusDefaultCheck"); int skinIndex = 0; foreach (var skinInfo in sortedSkins) { int row = skinIndex / boxesPerRow; int col = skinIndex % boxesPerRow; int rowOffset = row * (boxHeightPx + boxSpacing); int offsetMin = -rowOffset - boxHeightPx; int offsetMax = -rowOffset; float boxWidth = 0.45f; float boxSpacingX = 0.05f; float startX = 0.025f; float xMin = startX + (col * (boxWidth + boxSpacingX)); float xMax = xMin + boxWidth; string boxPanelName = $"SkinBox_{skinInfo.ContentId}"; elements.Add(new CuiElement { Name = boxPanelName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = $"{xMin} 0.998", AnchorMax = $"{xMax} 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = "1 1 1 0.15" } } }); string imageName = (skinInfo.SkinName == "Default" || skinInfo.SkinName == tierName) ? tierName : skinInfo.SkinName; string imagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", imageName) : null; if (!string.IsNullOrEmpty(imagePng)) { elements.Add(new CuiElement { Parent = boxPanelName, Name = $"SkinBox_Image_{skinInfo.ContentId}", Components = { new CuiRectTransformComponent { AnchorMin = "0 0.255", AnchorMax = "1 1" }, new CuiRawImageComponent { Png = imagePng, Color = "1 1 1 1" } } }); } else { elements.Add(new CuiElement { Parent = boxPanelName, Name = $"SkinBox_Image_{skinInfo.ContentId}", Components = { new CuiRectTransformComponent { AnchorMin = "0 0.255", AnchorMax = "1 1" }, new CuiRawImageComponent { Url = _config.SkinPreviewPlaceholderImageUrl, Color = "1 1 1 1" } } }); } if (skinInfo.ContentId == 10221 && tier == BuildingGrade.Enum.Metal) { CreateLabel(ref elements, "0.21 0", "1 0.245", "1 1 1 1", "0 0 0 0.5", skinInfo.SkinName, 26, TextAnchor.MiddleCenter, boxPanelName); } else { CreateLabel(ref elements, "0 0", "1 0.245", "1 1 1 1", "0 0 0 0.5", skinInfo.SkinName, 26, TextAnchor.MiddleCenter, boxPanelName); } elements.Add(new CuiButton { Button = { Command = $"cupboardplus.applyskin {skinInfo.ContentId}", Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, boxPanelName, $"SkinBox_Button_{skinInfo.ContentId}"); string defaultBtnName = $"DefaultSkinBtn_{skinInfo.ContentId}"; elements.Add(new CuiButton { Button = { Command = $"cupboardplus.setdefaultskin {skinInfo.ContentId}", Color = "0 0 0 1" }, RectTransform = { AnchorMin = "0 0.86", AnchorMax = "0.14 1" } }, boxPanelName, defaultBtnName); if (skinInfo.ContentId == defaultContentId && !string.IsNullOrEmpty(defaultCheckmarkPng)) { elements.Add(new CuiElement { Name = $"DefaultSkinCheck_{skinInfo.ContentId}", Parent = defaultBtnName, Components = { new CuiRawImageComponent { Png = defaultCheckmarkPng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0.1 0.1", AnchorMax = "0.9 0.9" } } }); } if (skinInfo.ContentId == 10221 && tier == BuildingGrade.Enum.Metal) { string colorBoxColor = "1 1 1 1"; bool isRandomColor = false; if (_selectedColorIndex.TryGetValue(player.userID, out var selectedColorIdx)) { if (selectedColorIdx == 0) { isRandomColor = true; if (!_randomColorIndex.TryGetValue(player.userID, out var currentIdx)) { currentIdx = 1; } string colorStr = GetColorString(currentIdx); colorBoxColor = $"{colorStr} 1"; } else { string colorStr = GetColorString(selectedColorIdx); colorBoxColor = $"{colorStr} 1"; } } string colorBoxName = $"ColorBox_{skinInfo.ContentId}"; elements.Add(new CuiButton { Button = { Command = $"cupboardplus.applycontainercolor {skinInfo.ContentId}", Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "0.21 0.245" } }, boxPanelName, colorBoxName); elements.Add(new CuiElement { Name = "ContainerColorBox_Background", Parent = colorBoxName, Components = { new CuiImageComponent { Color = colorBoxColor }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); if (!string.IsNullOrEmpty(sprayCanImagePng)) { elements.Add(new CuiElement { Name = "ContainerColorBox_SprayIcon", Parent = colorBoxName, Components = { new CuiRawImageComponent { Png = sprayCanImagePng, Color = "1 1 1 0.9" }, new CuiRectTransformComponent { AnchorMin = "0.15 0.15", AnchorMax = "0.85 0.85" } } }); } if (isRandomColor) { if (!_randomColorTimers.ContainsKey(player.userID)) { StartRandomColorAnimation(player); } } else { if (!_settingsPanelOpen.ContainsKey(player.userID) || !_settingsPanelOpen[player.userID]) { StopRandomColorAnimation(player); } } } skinIndex++; } const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closeskin", 15); _skinPanelContentIds[player.userID] = sortedSkins.Select(s => s.ContentId).ToList(); CuiHelper.AddUi(player, elements); } private void UpdateDefaultSkinCheckmarks(BasePlayer player, int defaultContentId) { if (player == null) return; if (!_skinPanelContentIds.TryGetValue(player.userID, out var contentIds) || contentIds == null || contentIds.Count == 0) return; string defaultCheckmarkPng = null; if (ImageLibrary != null) defaultCheckmarkPng = (string)ImageLibrary.Call("GetImage", "CupboardPlusDefaultCheck"); foreach (int contentId in contentIds) { CuiHelper.DestroyUi(player, $"DefaultSkinBtn_{contentId}"); } var elements = new CuiElementContainer(); foreach (int contentId in contentIds) { string boxPanelName = $"SkinBox_{contentId}"; string defaultBtnName = $"DefaultSkinBtn_{contentId}"; elements.Add(new CuiButton { Button = { Command = $"cupboardplus.setdefaultskin {contentId}", Color = "0 0 0 1" }, RectTransform = { AnchorMin = "0 0.86", AnchorMax = "0.14 1" } }, boxPanelName, defaultBtnName); if (contentId == defaultContentId && !string.IsNullOrEmpty(defaultCheckmarkPng)) { elements.Add(new CuiElement { Name = $"DefaultSkinCheck_{contentId}", Parent = defaultBtnName, Components = { new CuiRawImageComponent { Png = defaultCheckmarkPng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0.1 0.1", AnchorMax = "0.9 0.9" } } }); } } CuiHelper.AddUi(player, elements); } private int GetDefaultSkinContentId(BasePlayer player, BuildingGrade.Enum tier) { if (player == null) return 0; var playerData = GetPlayerData(player.userID); string tierKey = tier.ToString(); if (playerData.DefaultSkinPerTier.TryGetValue(tierKey, out int contentId)) return contentId; return 0; } private void CheckAndCacheOwnedSkins(BasePlayer player) { if (player == null) { _ownedSkinCache[0] = new HashSet(); return; } var allSkins = GetAllTierSkins(); var ownedContentIds = new HashSet(); foreach (var skin in allSkins) { if (skin.ContentId == 0) { ownedContentIds.Add(0); break; } } foreach (var skin in allSkins) { if (skin.ContentId == 0) continue; try { bool ownsSkin = CheckContentOwnership(player, skin.ContentId); if (ownsSkin) { ownedContentIds.Add(skin.ContentId); } } catch (Exception ex) { } } _ownedSkinCache[player.userID] = ownedContentIds; } private class TierSkinInfo { public BuildingGrade.Enum Tier; public string TierName; public string SkinName; public int ContentId; } private List GetAllTierSkins() { var tierSkins = new Dictionary> { [BuildingGrade.Enum.Wood] = new List<(int, string)> { (0, "Wood"), (10232, "Legacy Wood") }, [BuildingGrade.Enum.Stone] = new List<(int, string)> { (0, "Stone"), (10220, "Adobe"), (10223, "Brick"), (10225, "Brutalist"), (10326, "Jungle") }, [BuildingGrade.Enum.Metal] = new List<(int, string)> { (0, "Metal"), (10221, "Container") }, [BuildingGrade.Enum.TopTier] = new List<(int, string)> { (0, "Armored"), (10430, "Space Station") } }; var skins = new List(); foreach (var tier in tierSkins.Keys) { string tierName = GetTierName(tier); foreach (var (skinId, skinName) in tierSkins[tier]) { skins.Add(new TierSkinInfo { Tier = tier, TierName = tierName, SkinName = skinName, ContentId = skinId }); } } return skins.OrderBy(s => s.Tier).ThenBy(s => s.SkinName).ToList(); } private List GetAvailableTierSkins(List blocks) { var allSkins = GetAllTierSkins(); var tiersInBlocks = blocks.Select(b => b.grade).Distinct().ToList(); return allSkins.Where(s => tiersInBlocks.Contains(s.Tier) || s.Tier == BuildingGrade.Enum.TopTier).ToList(); } private void ApplySkinToBuilding(BasePlayer player, BuildingPrivlidge privilege, int contentId, BuildingGrade.Enum? targetTier = null) { var blocks = GetConnectedBuildingBlocks(privilege, player); if (blocks.Count == 0) { player.ChatMessage(Lang("Error.NoBlocks", player.UserIDString)); return; } if (contentId != 0 && PlayerDLCAPI != null) { try { object initializedResult = PlayerDLCAPI.Call("Initialized"); bool isInitialized = initializedResult is bool && (bool)initializedResult; if (!isInitialized) { player.ChatMessage(Lang("Error.SkinOwnershipUnavailable", player.UserIDString)); return; } bool ownsSkin = CheckContentOwnership(player, contentId); if (!ownsSkin) { player.ChatMessage(Lang("Error.SkinNotOwned", player.UserIDString)); return; } } catch (Exception ex) { player.ChatMessage(Lang("Error.SkinOwnershipError", player.UserIDString)); return; } } else if (contentId != 0 && PlayerDLCAPI == null) { player.ChatMessage(Lang("Error.SkinOwnershipAdmin", player.UserIDString)); return; } var eligibleBlocks = new List(); foreach (var block in blocks) { if (block == null || block.IsDestroyed) continue; if (targetTier.HasValue && block.grade != targetTier.Value) continue; if (block.skinID == (ulong)contentId) continue; bool isValid = false; if (block.blockDefinition?.grades != null) { foreach (var grade in block.blockDefinition.grades) { if (grade?.gradeBase != null && grade.gradeBase.type == block.grade && grade.gradeBase.skin == (ulong)contentId) { isValid = true; break; } } } if (isValid) { eligibleBlocks.Add(block); } } if (eligibleBlocks.Count == 0) { string tierName = targetTier.HasValue ? GetTierName(targetTier.Value) : "all tiers"; string skinName = GetSkinName(contentId, targetTier); player.ChatMessage(Lang("Success.AllBlocksHaveSkin", player.UserIDString, tierName, skinName)); return; } var playerData = GetPlayerData(player.userID); if (targetTier.HasValue) { string tierKey = targetTier.Value.ToString(); playerData.LastUsedSkin[tierKey] = contentId; playerData.DefaultSkinPerTier[tierKey] = contentId; } else { var uniqueTiers = eligibleBlocks.Select(b => b.grade).Distinct(); foreach (var tier in uniqueTiers) { string tierKey = tier.ToString(); playerData.LastUsedSkin[tierKey] = contentId; playerData.DefaultSkinPerTier[tierKey] = contentId; } } SaveData(); CloseSkinUI(player); StartSkinQueue(player, privilege, eligibleBlocks, contentId, targetTier); } private void CreateTierTabs(ref CuiElementContainer container, string parent, List availableTiers, BasePlayer player, BuildingGrade.Enum? selectedTier) { var tiers = new[] { BuildingGrade.Enum.Wood, BuildingGrade.Enum.Stone, BuildingGrade.Enum.Metal, BuildingGrade.Enum.TopTier }; var tierNames = new[] { "WOOD", "STONE", "METAL", "ARMORED" }; float tabWidth = 0.23f; float startX = 0.02f; float yMin = 0.75f; float yMax = 0.8f; for (int i = 0; i < tiers.Length; i++) { var tier = tiers[i]; var tierName = tierNames[i]; bool isAvailable = availableTiers.Contains(tier); bool isSelected = selectedTier.HasValue && selectedTier.Value == tier; float xMin = startX + (i * (tabWidth + 0.01f)); float xMax = xMin + tabWidth; const string buttonColor = "1 1 1 0.15"; string tabColor = isAvailable ? (isSelected ? buttonColor : buttonColor) : "0.1 0.1 0.1 0.5"; string textColor = isAvailable ? "1 1 1 1" : "0.5 0.5 0.5 1"; string tabPanel = CreatePanel(ref container, $"{xMin} {yMin}", $"{xMax} {yMax}", tabColor, parent, $"TierTab_{tier}"); CreatePanel(ref container, "0 0", "1 1", "0 0 0 0", tabPanel, $"TierTab_Border_{tier}"); CreateLabel(ref container, "0 0.1", "1 0.9", textColor, "0 0 0 0", tierName, 14, TextAnchor.MiddleCenter, tabPanel); if (isAvailable) { container.Add(new CuiButton { Button = { Command = $"cupboardplus.selecttier {tier}", Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, tabPanel, $"TierTab_Button_{tier}"); } } } private void RefreshContentPanel(BasePlayer player, BuildingGrade.Enum targetGrade) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } _selectedTiers[player.userID] = targetGrade; var blocks = GetConnectedBuildingBlocks(privilege, player); var blocksByGrade = blocks.GroupBy(b => b.grade).ToDictionary(g => g.Key, g => g.ToList()); var eligibleBlocks = new List(); foreach (var blockGroup in blocksByGrade) { var gradeBlocks = blockGroup.Value.Where(b => b.grade < targetGrade && CanUpgradeToTier(b.grade, targetGrade)).ToList(); eligibleBlocks.AddRange(gradeBlocks); } var downgradeableBlocks = new List(); if (_config.EnableDowngrade) { foreach (var block in blocks) { if (block.grade > targetGrade && CanUpgradeToTier(block.grade, targetGrade)) { downgradeableBlocks.Add(block); } } } if (eligibleBlocks.Count == 0 && downgradeableBlocks.Count == 0) { var errorElements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "ContentPanel"); string errorPanel = CreatePanel(ref errorElements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "UpgradeMainPanel", "ContentPanel"); CreateLabel(ref errorElements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "No blocks can be upgraded or downgraded to this tier", 18, TextAnchor.MiddleCenter, errorPanel); CuiHelper.AddUi(player, errorElements); return; } var totalCost = CalculateTotalCost(eligibleBlocks, targetGrade); if (eligibleBlocks.Count > 0 && totalCost.Count == 0) { var errorElements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "ContentPanel"); string errorPanel = CreatePanel(ref errorElements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "UpgradeMainPanel", "ContentPanel"); CreateLabel(ref errorElements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "Error: Could not determine upgrade costs", 18, TextAnchor.MiddleCenter, errorPanel); CuiHelper.AddUi(player, errorElements); return; } bool noCost = HasNoCostPermission(player); var available = GetAvailableResources(privilege); bool canFullUpgrade = eligibleBlocks.Count > 0 && (noCost || CanAffordUpgrade(available, totalCost, eligibleBlocks.Count)); int canUpgradeCount = eligibleBlocks.Count > 0 ? (noCost ? eligibleBlocks.Count : CalculateAffordableBlocks(available, eligibleBlocks, targetGrade)) : 0; var availableTiers = GetAvailableUpgradeTiers(blocks); var tabElements = new CuiElementContainer(); var tiers = new[] { BuildingGrade.Enum.Wood, BuildingGrade.Enum.Stone, BuildingGrade.Enum.Metal, BuildingGrade.Enum.TopTier }; foreach (var tier in tiers) { CuiHelper.DestroyUi(player, $"TierTab_{tier}"); CuiHelper.DestroyUi(player, $"TierTab_Border_{tier}"); CuiHelper.DestroyUi(player, $"TierTab_TopBorder_{tier}"); CuiHelper.DestroyUi(player, $"TierTab_Button_{tier}"); } CreateTierTabs(ref tabElements, "UpgradeMainPanel", availableTiers, player, targetGrade); CuiHelper.AddUi(player, tabElements); var elements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "ContentPanel"); string contentPanel = CreatePanel(ref elements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "UpgradeMainPanel", "ContentPanel"); string tierName = GetTierName(targetGrade); string headerSection = CreatePanel(ref elements, "0.02 0.88", "0.98 0.98", "0.2 0.2 0.2 0.6", contentPanel, "HeaderSection"); string headerText = eligibleBlocks.Count > 0 ? $"Upgrade to {tierName}" : $"Downgrade to {tierName}"; CreateLabel(ref elements, "0 0", "1 1", "1 1 1 1", "0 0 0 0", headerText, 22, TextAnchor.MiddleCenter, headerSection); string summarySection = CreatePanel(ref elements, "0.02 0.75", "0.98 0.85", "0.18 0.18 0.18 0.4", contentPanel, "SummarySection"); CreateLabel(ref elements, "0.05 0.2", "0.95 0.8", "0.8 0.8 0.8 1", "0 0 0 0", "Building Pieces", 14, TextAnchor.MiddleLeft, summarySection); int displayCount = eligibleBlocks.Count > 0 ? eligibleBlocks.Count : downgradeableBlocks.Count; CreateLabel(ref elements, "0.05 0.2", "0.95 0.8", "1 1 1 1", "0 0 0 0", $"{displayCount}", 20, TextAnchor.MiddleRight, summarySection); var blockCounts = (eligibleBlocks.Count > 0 ? eligibleBlocks : downgradeableBlocks) .GroupBy(b => b.ShortPrefabName) .Select(g => new { Type = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Count) .ToList(); CreateLabel(ref elements, "0.05 0.68", "0.95 0.72", "0.7 0.7 0.7 1", "0 0 0 0", "Building Pieces Breakdown", 16, TextAnchor.MiddleLeft, contentPanel); string scrollContainerName = "PiecesScrollContainer"; string scrollViewName = "PiecesScrollView"; string scrollContainer = CreatePanel(ref elements, "0.05 0.28", "0.95 0.76", "0.12 0.12 0.12 0.3", contentPanel, scrollContainerName); int buttonSpacing = 24; int buttonSize = 20; int numberOfItems = blockCounts.Count; int panelSize = -buttonSpacing * numberOfItems; elements.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0.12 0.12 0.12 0.3" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = UnityEngine.UI.ScrollRect.MovementType.Elastic, Elasticity = 0.2f, Inertia = true, DecelerationRate = 0.2f, 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" } } }); int itemIndex = 0; foreach (var blockType in blockCounts) { string displayName = blockType.Type .Replace("foundation.", "") .Replace("wall.", "") .Replace("window.", "") .Replace("door.", "") .Replace("roof.", "") .Replace("floor.", "") .Replace("stairs.", "") .Replace("frame.", "") .Replace("floor.frame.", "") .Replace(".", " "); displayName = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(displayName); var offsetMin = -itemIndex * buttonSpacing - buttonSize; var offsetMax = -itemIndex * buttonSpacing; string pieceRowName = $"PieceRow_{blockType.Type}"; elements.Add(new CuiElement { Name = pieceRowName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0.05 0.998", AnchorMax = "0.95 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = "0.18 0.18 0.18 0.4" } } }); CreateLabel(ref elements, "0.05 0", "0.7 1", "1 1 1 1", "0 0 0 0", displayName, 14, TextAnchor.MiddleLeft, pieceRowName); CreateLabel(ref elements, "0.7 0", "0.95 1", "1 1 1 1", "0 0 0 0", blockType.Count.ToString(), 14, TextAnchor.MiddleRight, pieceRowName); itemIndex++; } CreateLabel(ref elements, "0.05 0.32", "0.95 0.36", "0.7 0.7 0.7 1", "0 0 0 0", "Required Resources", 16, TextAnchor.MiddleLeft, contentPanel); float costY = 0.25f; float costSpacing = 0.08f; float costRowHeight = 0.06f; if (noCost) { string costRow = CreatePanel(ref elements, $"0.05 {costY - costRowHeight}", $"0.95 {costY}", "0.15 0.25 0.15 0.5", contentPanel, "CostRow_FREE"); CreateLabel(ref elements, "0.05 0", "0.5 1", "0.9 0.9 0.9 1", "0 0 0 0", "Cost", 15, TextAnchor.MiddleLeft, costRow); CreateLabel(ref elements, "0.5 0", "0.95 1", "0.3 1 0.3 1", "0 0 0 0", "FREE", 15, TextAnchor.MiddleRight, costRow); } else { foreach (var kvp in totalCost.OrderByDescending(x => x.Value)) { var itemDef = ItemManager.FindItemDefinition(kvp.Key); if (itemDef == null) continue; int have = available.TryGetValue(kvp.Key, out var amt) ? amt : 0; int needed = kvp.Value; string itemName = itemDef.displayName?.english ?? itemDef.shortname; bool canAfford = have >= needed; string rowBgColor = canAfford ? "0.15 0.25 0.15 0.5" : "0.25 0.15 0.15 0.5"; string costRow = CreatePanel(ref elements, $"0.05 {costY - costRowHeight}", $"0.95 {costY}", rowBgColor, contentPanel, $"CostRow_{kvp.Key}"); CreateLabel(ref elements, "0.05 0", "0.5 1", "0.9 0.9 0.9 1", "0 0 0 0", itemName, 15, TextAnchor.MiddleLeft, costRow); string amountColor = canAfford ? "0.3 1 0.3 1" : "1 0.7 0.7 1"; string amountText = $"{needed:N0} / {have:N0}"; CreateLabel(ref elements, "0.5 0", "0.95 1", amountColor, "0 0 0 0", amountText, 15, TextAnchor.MiddleRight, costRow); costY -= costSpacing; } } const string actionButtonColor = "1 1 1 0.15"; var damagedBlocks = GetDamagedBlocks(privilege, player); var repairCost = CalculateTotalRepairCost(damagedBlocks); var downgradeCost = CalculateTotalCost(downgradeableBlocks, targetGrade); bool showDowngrade = _config.EnableDowngrade && HasDowngradePermission(player) && downgradeableBlocks.Count > 0; float buttonHeight = 0.08f; float verticalButtonSpacing = 0.01f; float upgradeYMin = showDowngrade ? (0.02f + buttonHeight + verticalButtonSpacing) : 0.02f; float upgradeYMax = showDowngrade ? (0.1f + buttonHeight + verticalButtonSpacing) : 0.1f; if (canFullUpgrade) { CreateActionButton(ref elements, contentPanel, $"0.05 {upgradeYMin}", $"0.48 {upgradeYMax}", actionButtonColor, "Upgrade", $"cupboardplus.performupgrade {targetGrade} full", 15); } else if (canUpgradeCount > 0) { CreateActionButton(ref elements, contentPanel, $"0.05 {upgradeYMin}", $"0.48 {upgradeYMax}", actionButtonColor, "🔧 Partial", $"cupboardplus.performupgrade {targetGrade} partial", 15); } if (showDowngrade) { CreateActionButton(ref elements, contentPanel, "0.05 0.02", "0.48 0.1", actionButtonColor, "⬇ Downgrade", $"cupboardplus.performdowngrade {targetGrade}", 15); } CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closeui", 15); CuiHelper.AddUi(player, elements); } private void CreateActionButton(ref CuiElementContainer container, string parent, string anchorMin, string anchorMax, string color, string text, string command, int fontSize, string buttonName = null) { var button = new CuiButton { Button = { Color = color, Command = command }, RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }, Text = { Text = text, FontSize = fontSize, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1", Font = "robotocondensed-bold.ttf" } }; if (!string.IsNullOrEmpty(buttonName)) { container.Add(button, parent, buttonName); } else { container.Add(button, parent); } } private string CreatePanel(ref CuiElementContainer container, string anchorMin, string anchorMax, string panelColor, string parent = "Overlay", string panelName = null, bool isMainPanel = false, bool blur = false) { var panel = new CuiPanel { RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }, Image = { Color = panelColor }, CursorEnabled = isMainPanel }; if (blur) { panel.Image.Material = "assets/content/ui/uibackgroundblur.mat"; } string name = container.Add(panel, parent, panelName); return name; } private void CreateLabel(ref CuiElementContainer container, string anchorMin, string anchorMax, string textColor, string backgroundColor, string text, int fontSize, TextAnchor alignment, string parent = "Overlay") { string panel = CreatePanel(ref container, anchorMin, anchorMax, backgroundColor, parent); container.Add(new CuiLabel { Text = { Color = textColor, Text = text, Align = alignment, FontSize = fontSize, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, panel); } private void CreateSettingsButton(ref CuiElementContainer container, BasePlayer player) { if (!_openPanels.ContainsKey(player.userID)) { return; } const string settingsIconName = "CupboardPlusSettings"; string settingsIconPng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", settingsIconName) : null; string settingsButtonName = "SettingsButton"; container.Add(new CuiButton { Button = { Command = "cupboardplus.opensettings", Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0.3 0.8", AnchorMax = "0.33 0.855" } }, "Overlay", settingsButtonName); if (!string.IsNullOrEmpty(settingsIconPng)) { container.Add(new CuiElement { Parent = settingsButtonName, Components = { new CuiRawImageComponent { Png = settingsIconPng, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } else if (!string.IsNullOrEmpty(_config.SettingsIconUrl)) { container.Add(new CuiElement { Parent = settingsButtonName, Components = { new CuiRawImageComponent { Url = _config.SettingsIconUrl, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } } private void CreateColorPaletteGrid(ref CuiElementContainer container, string parent) { float gridWidth = 0.95f - 0.05f; float gridHeight = 0.5f - 0.15f; float swatchWidth = (gridWidth - (5 - 1) * 0.02f) / 5; float swatchHeight = (gridHeight - (4 - 1) * 0.02f) / 4; for (int row = 0; row < 4; row++) { for (int col = 0; col < 5; col++) { int colorIndex = row * 5 + col; if (colorIndex >= ColorPalette.Length) break; float xMin = 0.05f + col * (swatchWidth + 0.02f); float yMin = 0.15f + (4 - 1 - row) * (swatchHeight + 0.02f); float xMax = xMin + swatchWidth; float yMax = yMin + swatchHeight; string color = ColorPalette[colorIndex]; string swatchName = $"ColorSwatch_{row}_{col}"; container.Add(new CuiButton { Button = { Command = $"cupboardplus.selectcolor {colorIndex}", Color = "0 0 0 0" }, RectTransform = { AnchorMin = $"{xMin} {yMin}", AnchorMax = $"{xMax} {yMax}" } }, parent, swatchName); string backgroundName = colorIndex == 0 ? "RandomColorSwatch_Background" : $"ColorSwatchBg_{row}_{col}"; container.Add(new CuiElement { Name = backgroundName, Parent = swatchName, Components = { new CuiImageComponent { Color = $"{color} 1" }, new CuiRectTransformComponent { AnchorMin = "0.1 0.1", AnchorMax = "0.9 0.9" } } }); } } } private void OpenSettingsPanel(BasePlayer player) { if (!_openPanels.ContainsKey(player.userID)) { CuiHelper.DestroyUi(player, "SettingsPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); _settingsPanelOpen.Remove(player.userID); return; } CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); var elements = new CuiElementContainer(); var settingsPanel = CreatePanel(ref elements, "0.15 0.2", "0.33 0.855", "0.12 0.12 0.12 0.95", "Overlay", "SettingsPanel", true, true); CreateLabel(ref elements, "0.05 0.9", "0.95 0.98", "1 1 1 1", "0 0 0 0", "Settings", 20, TextAnchor.MiddleCenter, settingsPanel); if (!_skinUpgradeEffectEnabled.TryGetValue(player.userID, out var enabled)) { var playerData = GetPlayerData(player.userID); enabled = playerData.UpgradeEffectEnabled; _skinUpgradeEffectEnabled[player.userID] = enabled; } bool upgradeEffectEnabled = enabled; string upgradeEffectText = upgradeEffectEnabled ? "Upgrade Effect: ON" : "Upgrade Effect: OFF"; const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, settingsPanel, "0.05 0.75", "0.95 0.85", actionButtonColor, upgradeEffectText, "cupboardplus.toggleupgradeeffect", 16); var playerDataForSettings = GetPlayerData(player.userID); bool wallpaperAdvancedMode = playerDataForSettings.WallpaperAdvancedMode; string wallpaperAdvancedText = wallpaperAdvancedMode ? "Wallpaper Advanced Mode: ON" : "Wallpaper Advanced Mode: OFF"; CreateActionButton(ref elements, settingsPanel, "0.05 0.65", "0.95 0.74", actionButtonColor, wallpaperAdvancedText, "cupboardplus.togglewallpaperadvanced", 16); float colorPaletteYMin = 0.55f; float colorPaletteYMax = 0.64f; CreateLabel(ref elements, $"0.05 {colorPaletteYMin}", $"0.95 {colorPaletteYMax}", "1 1 1 0.7", "0 0 0 0", "Color Palette", 18, TextAnchor.MiddleCenter, settingsPanel); CreateColorPaletteGrid(ref elements, settingsPanel); string closeButtonColor = actionButtonColor; if (!_selectedColorIndex.TryGetValue(player.userID, out var selectedColorIdx)) { var playerData = GetPlayerData(player.userID); if (playerData.SelectedColorIndex >= 0) { selectedColorIdx = playerData.SelectedColorIndex; _selectedColorIndex[player.userID] = selectedColorIdx; } } if (selectedColorIdx >= 0 && selectedColorIdx < ColorPalette.Length) { string colorStr = GetColorString(selectedColorIdx); closeButtonColor = $"{colorStr} 0.8"; } CreateActionButton(ref elements, settingsPanel, "0.05 0.02", "0.95 0.12", closeButtonColor, "✕ Close", "cupboardplus.closesettings", 15, "SettingsCloseButton"); CuiHelper.AddUi(player, elements); if (_openPanels.ContainsKey(player.userID)) { _settingsPanelOpen[player.userID] = true; StartRandomColorAnimation(player); } else { CuiHelper.DestroyUi(player, "SettingsPanel"); } } private void UpdateSettingsCloseButtonColor(BasePlayer player) { string closeButtonColor = "1 1 1 0.15"; if (!_selectedColorIndex.TryGetValue(player.userID, out var selectedColorIdx)) { var playerData = GetPlayerData(player.userID); if (playerData.SelectedColorIndex >= 0) { selectedColorIdx = playerData.SelectedColorIndex; _selectedColorIndex[player.userID] = selectedColorIdx; } } if (selectedColorIdx >= 0 && selectedColorIdx < ColorPalette.Length) { string colorStr = GetColorString(selectedColorIdx); closeButtonColor = $"{colorStr} 0.8"; } CuiHelper.DestroyUi(player, "SettingsCloseButton"); var elements = new CuiElementContainer(); CreateActionButton(ref elements, "SettingsPanel", "0.05 0.02", "0.95 0.12", closeButtonColor, "✕ Close", "cupboardplus.closesettings", 15, "SettingsCloseButton"); CuiHelper.AddUi(player, elements); } private void StartRandomColorAnimation(BasePlayer player) { StopRandomColorAnimation(player); _randomColorIndex[player.userID] = 1; _randomColorTimers[player.userID] = timer.Repeat(0.5f, 0, () => UpdateRandomColorSwatch(player)); } private void StopRandomColorAnimation(BasePlayer player) { if (_randomColorTimers.TryGetValue(player.userID, out var timer)) { timer?.Destroy(); _randomColorTimers.Remove(player.userID); } _randomColorIndex.Remove(player.userID); } private void UpdateRandomColorSwatch(BasePlayer player) { bool settingsOpen = _settingsPanelOpen.ContainsKey(player.userID) && _settingsPanelOpen[player.userID]; bool skinPanelOpen = _openPanels.ContainsKey(player.userID) && _openPanels[player.userID] == "SkinPanel"; bool randomSelected = _selectedColorIndex.TryGetValue(player.userID, out var selectedIdx) && selectedIdx == 0; if (!settingsOpen && (!skinPanelOpen || !randomSelected)) { StopRandomColorAnimation(player); return; } if (!_randomColorIndex.TryGetValue(player.userID, out int currentIndex)) { currentIndex = 1; } string colorStr = GetColorString(currentIndex); string color = $"{colorStr} 1"; var elements = new CuiElementContainer(); if (settingsOpen) { CuiHelper.DestroyUi(player, "RandomColorSwatch_Background"); string swatchParent = "ColorSwatch_0_0"; elements.Add(new CuiElement { Name = "RandomColorSwatch_Background", Parent = swatchParent, Components = { new CuiImageComponent { Color = color }, new CuiRectTransformComponent { AnchorMin = "0.1 0.1", AnchorMax = "0.9 0.9" } } }); } if (skinPanelOpen && randomSelected) { CuiHelper.DestroyUi(player, "ContainerColorBox_Background"); elements.Add(new CuiElement { Name = "ContainerColorBox_Background", Parent = "ColorBox_10221", Components = { new CuiImageComponent { Color = color }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); string sprayCanImagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", "CupboardPlusSprayCan") : null; if (!string.IsNullOrEmpty(sprayCanImagePng)) { CuiHelper.DestroyUi(player, "ContainerColorBox_SprayIcon"); elements.Add(new CuiElement { Name = "ContainerColorBox_SprayIcon", Parent = "ColorBox_10221", Components = { new CuiRawImageComponent { Png = sprayCanImagePng, Color = "1 1 1 0.9" }, new CuiRectTransformComponent { AnchorMin = "0.15 0.15", AnchorMax = "0.85 0.85" } } }); } } if (elements.Count > 0) { CuiHelper.AddUi(player, elements); } currentIndex++; if (currentIndex > 16) { currentIndex = 1; } _randomColorIndex[player.userID] = currentIndex; } #endregion #region Command Handlers [ConsoleCommand("cupboardplus.upgradeui")] private void CmdUpgradeUI(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasUpgradePermission(player)) return; if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "UpgradeMainPanel") { CloseUpgradeUI(player); return; } if (openPanel == "SkinPanel") { CuiHelper.DestroyUi(player, "SkinPanel"); CuiHelper.DestroyUi(player, "SkinContentPanel"); _skinSelectedTier.Remove(player.userID); } if (openPanel == "RepairMainPanel") { CuiHelper.DestroyUi(player, "RepairMainPanel"); CuiHelper.DestroyUi(player, "RepairContentPanel"); } if (openPanel == "WallpaperMainPanel") { CuiHelper.DestroyUi(player, "WallpaperMainPanel"); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); } if (openPanel == "AuthedUsersMainPanel") { CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); } CuiHelper.DestroyUi(player, "SettingsButton"); OpenUpgradeUI(player); } [ConsoleCommand("cupboardplus.selecttier")] private void CmdSelectTier(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasUpgradePermission(player)) return; if (arg.Args == null || arg.Args.Length == 0) return; if (Enum.TryParse(arg.Args[0], out var targetGrade)) { RefreshContentPanel(player, targetGrade); } } [ConsoleCommand("cupboardplus.performupgrade")] private void CmdPerformUpgrade(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasUpgradePermission(player)) return; if (arg.Args == null || arg.Args.Length < 2) return; if (!Enum.TryParse(arg.Args[0], out var targetGrade)) { if (!_selectedTiers.TryGetValue(player.userID, out var selected) || !selected.HasValue) { player.ChatMessage(Lang("Error.NoTierSelected", player.UserIDString)); return; } targetGrade = selected.Value; } string upgradeType = arg.Args[1]; PerformUpgrade(player, targetGrade, upgradeType == "full"); } [ConsoleCommand("cupboardplus.performrepair")] private void CmdPerformRepair(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasRepairPermission(player)) return; var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } var damagedBlocks = GetDamagedBlocks(privilege, player); if (damagedBlocks.Count == 0) { player.ChatMessage(Lang("Error.NoDamagedBlocks", player.UserIDString)); return; } CloseRepairUI(player); StartRepairQueue(player, privilege, damagedBlocks); } [ConsoleCommand("cupboardplus.repairui")] private void CmdRepairUI(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasRepairPermission(player)) return; if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "RepairMainPanel") { CloseRepairUI(player); return; } if (openPanel == "UpgradeMainPanel") { CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "ContentPanel"); _selectedTiers.Remove(player.userID); } if (openPanel == "SkinPanel") { CuiHelper.DestroyUi(player, "SkinPanel"); CuiHelper.DestroyUi(player, "SkinContentPanel"); _skinSelectedTier.Remove(player.userID); } if (openPanel == "WallpaperMainPanel") { CuiHelper.DestroyUi(player, "WallpaperMainPanel"); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); } if (openPanel == "AuthedUsersMainPanel") { CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); } CuiHelper.DestroyUi(player, "SettingsButton"); OpenRepairUI(player); } [ConsoleCommand("cupboardplus.performdowngrade")] private void CmdPerformDowngrade(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasDowngradePermission(player)) return; if (!_config.EnableDowngrade) { player.ChatMessage(Lang("Error.DowngradeDisabled", player.UserIDString)); return; } if (arg.Args == null || arg.Args.Length < 1) return; BuildingGrade.Enum targetGrade; if (!Enum.TryParse(arg.Args[0], out targetGrade)) { player.ChatMessage(Lang("Error.InvalidTier", player.UserIDString)); return; } var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } var blocks = GetConnectedBuildingBlocks(privilege, player); var downgradeableBlocks = blocks.Where(b => b.grade > targetGrade && CanUpgradeToTier(b.grade, targetGrade)).ToList(); if (downgradeableBlocks.Count == 0) { player.ChatMessage(Lang("Error.NoBlocksToDowngrade", player.UserIDString)); return; } CloseUpgradeUI(player); StartDowngradeQueue(player, privilege, downgradeableBlocks, targetGrade); } private void CloseUpgradeUI(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "ContentPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _selectedTiers.Remove(player.userID); _openPanels.Remove(player.userID); _settingsPanelOpen.Remove(player.userID); StopRandomColorAnimation(player); } private void CloseSkinUI(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, "SkinPanel"); CuiHelper.DestroyUi(player, "SkinContentPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _skinSelectedTier.Remove(player.userID); _openPanels.Remove(player.userID); _ownedSkinCache.Remove(player.userID); _settingsPanelOpen.Remove(player.userID); StopRandomColorAnimation(player); } private void OpenRepairUI(BasePlayer player) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } CuiHelper.DestroyUi(player, "RepairMainPanel"); _openPanels[player.userID] = "RepairMainPanel"; var elements = new CuiElementContainer(); var panel = CreatePanel(ref elements, "0.3435 0.121", "0.64 0.855", _uiSettings.BackgroundColor, "Overlay", "RepairMainPanel", true, true); const string headerImageName = "CupboardPlusHeader"; string headerImagePng = null; string headerImageUrl = null; if (ImageLibrary != null) { headerImagePng = (string)ImageLibrary.Call("GetImage", headerImageName); if (string.IsNullOrEmpty(headerImagePng)) { headerImageUrl = (string)ImageLibrary.Call("GetImage", headerImageName, 0, true); } } if (string.IsNullOrEmpty(headerImagePng) && string.IsNullOrEmpty(headerImageUrl)) { headerImageUrl = _config.HeaderImageUrl; } elements.Add(new CuiElement { Parent = panel, Name = "RepairHeaderImage", Components = { new CuiRectTransformComponent { AnchorMin = "0.01 0.84", AnchorMax = "0.99 0.99" }, !string.IsNullOrEmpty(headerImagePng) ? new CuiRawImageComponent { Png = headerImagePng, Color = "1 1 1 1" } : new CuiRawImageComponent { Url = headerImageUrl, Color = "1 1 1 1" } } }); if (!_settingsPanelOpen.TryGetValue(player.userID, out var settingsOpen) || !settingsOpen) { CreateSettingsButton(ref elements, player); } CuiHelper.AddUi(player, elements); RefreshRepairContentPanel(player); } private void RefreshRepairContentPanel(BasePlayer player) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } var blocks = GetConnectedBuildingBlocks(privilege, player); var damagedBlocks = GetDamagedBlocks(privilege, player); if (damagedBlocks.Count == 0) { var errorElements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "RepairContentPanel"); string errorPanel = CreatePanel(ref errorElements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "RepairMainPanel", "RepairContentPanel"); CreateLabel(ref errorElements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "No damaged blocks found", 18, TextAnchor.MiddleCenter, errorPanel); const string cancelButtonColor = "1 1 1 0.15"; CreateActionButton(ref errorElements, errorPanel, "0.52 0.02", "0.95 0.1", cancelButtonColor, "✕ Cancel", "cupboardplus.closerepair", 15); CuiHelper.AddUi(player, errorElements); return; } var totalCost = CalculateTotalRepairCost(damagedBlocks); if (totalCost.Count == 0) { var errorElements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "RepairContentPanel"); string errorPanel = CreatePanel(ref errorElements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "RepairMainPanel", "RepairContentPanel"); CreateLabel(ref errorElements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "Error: Could not determine repair costs", 18, TextAnchor.MiddleCenter, errorPanel); const string cancelButtonColor = "1 1 1 0.15"; CreateActionButton(ref errorElements, errorPanel, "0.52 0.02", "0.95 0.1", cancelButtonColor, "✕ Cancel", "cupboardplus.closerepair", 15); CuiHelper.AddUi(player, errorElements); return; } bool noCost = HasNoCostPermission(player); var available = GetAvailableResources(privilege); bool canRepair = noCost || CanAffordRepair(available, totalCost); var elements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "RepairContentPanel"); string contentPanel = CreatePanel(ref elements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", "RepairMainPanel", "RepairContentPanel"); string headerSection = CreatePanel(ref elements, "0.02 0.88", "0.98 0.98", "0.2 0.2 0.2 0.6", contentPanel, "RepairHeaderSection"); CreateLabel(ref elements, "0 0", "1 1", "1 1 1 1", "0 0 0 0", "Repair Blocks", 22, TextAnchor.MiddleCenter, headerSection); string summarySection = CreatePanel(ref elements, "0.02 0.75", "0.98 0.85", "0.18 0.18 0.18 0.4", contentPanel, "RepairSummarySection"); CreateLabel(ref elements, "0.05 0.2", "0.95 0.8", "0.8 0.8 0.8 1", "0 0 0 0", "Damaged Blocks", 14, TextAnchor.MiddleLeft, summarySection); CreateLabel(ref elements, "0.05 0.2", "0.95 0.8", "1 1 1 1", "0 0 0 0", $"{damagedBlocks.Count}", 20, TextAnchor.MiddleRight, summarySection); var blocksByTier = damagedBlocks .GroupBy(b => b.grade) .OrderBy(g => g.Key) .ToList(); string scrollContainerName = "RepairPiecesScrollContainer"; string scrollViewName = "RepairPiecesScrollView"; string scrollContainer = CreatePanel(ref elements, "0.05 0.27", "0.95 0.75", "0.12 0.12 0.12 0.3", contentPanel, scrollContainerName); int totalRows = blocksByTier.Count; foreach (var tierGroup in blocksByTier) { totalRows += tierGroup.Count(); } int buttonSpacing = 24; int buttonSize = 20; int panelSize = -buttonSpacing * totalRows; elements.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0.12 0.12 0.12 0.3" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = UnityEngine.UI.ScrollRect.MovementType.Elastic, Elasticity = 0.2f, Inertia = true, DecelerationRate = 0.2f, 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" } } }); int itemIndex = 0; foreach (var tierGroup in blocksByTier) { string tierName = GetTierName(tierGroup.Key); var tierHeaderOffsetMin = -itemIndex * buttonSpacing - buttonSize; var tierHeaderOffsetMax = -itemIndex * buttonSpacing; string tierHeaderRow = $"TierHeader_{tierGroup.Key}"; elements.Add(new CuiElement { Name = tierHeaderRow, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0.05 0.998", AnchorMax = "0.95 0.998", OffsetMin = $"0 {tierHeaderOffsetMin}", OffsetMax = $"0 {tierHeaderOffsetMax}" }, new CuiImageComponent { Color = "0.25 0.25 0.25 0.6" } } }); CreateLabel(ref elements, "0.05 0", "0.95 1", "1 0.8 0.4 1", "0 0 0 0", tierName, 16, TextAnchor.MiddleCenter, tierHeaderRow); itemIndex++; foreach (var block in tierGroup.OrderBy(b => b.ShortPrefabName)) { float currentHP = block.health; float maxHP = GetMaxHPForTier(block.grade); float hpMissing = maxHP - currentHP; var repairCost = GetRepairCost(block); string costText = "N/A"; if (repairCost != null && repairCost.Count > 0 && repairCost[0]?.itemDef != null) { var itemDef = repairCost[0].itemDef; float amount = repairCost[0].amount; string resourceName = itemDef.displayName?.english ?? itemDef.shortname; costText = $"Cost {Mathf.CeilToInt(amount)} {resourceName}"; } string displayName = block.ShortPrefabName .Replace("foundation.", "") .Replace("wall.", "") .Replace("window.", "") .Replace("door.", "") .Replace("roof.", "") .Replace("floor.", "") .Replace("stairs.", "") .Replace("frame.", "") .Replace("floor.frame.", "") .Replace(".", " "); displayName = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(displayName); var offsetMin = -itemIndex * buttonSpacing - buttonSize; var offsetMax = -itemIndex * buttonSpacing; string pieceRowName = $"RepairPieceRow_{block.net.ID}"; elements.Add(new CuiElement { Name = pieceRowName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0.05 0.998", AnchorMax = "0.95 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = "0.18 0.18 0.18 0.4" } } }); CreateLabel(ref elements, "0.05 0", "0.4 1", "1 1 1 1", "0 0 0 0", displayName, 14, TextAnchor.MiddleLeft, pieceRowName); string hpText = $"{currentHP:F0}/{maxHP:F0} HP"; CreateLabel(ref elements, "0.4 0", "0.65 1", "0.9 0.9 0.9 1", "0 0 0 0", hpText, 13, TextAnchor.MiddleCenter, pieceRowName); CreateLabel(ref elements, "0.65 0", "0.95 1", "1 1 1 1", "0 0 0 0", costText, 13, TextAnchor.MiddleRight, pieceRowName); itemIndex++; } } float costY = 0.18f; float costSpacing = 0.08f; float costRowHeight = 0.06f; if (noCost) { string costRow = CreatePanel(ref elements, $"0.05 {costY - costRowHeight}", $"0.95 {costY}", "0.15 0.25 0.15 0.5", contentPanel, "RepairCostRow_FREE"); CreateLabel(ref elements, "0.05 0", "0.5 1", "0.9 0.9 0.9 1", "0 0 0 0", "Total Cost", 15, TextAnchor.MiddleLeft, costRow); CreateLabel(ref elements, "0.5 0", "0.95 1", "0.3 1 0.3 1", "0 0 0 0", "FREE", 15, TextAnchor.MiddleRight, costRow); } else { CreateLabel(ref elements, "0.05 0.19", "0.95 0.25", "0.9 0.9 0.9 1", "0 0 0 0", "Total Cost", 16, TextAnchor.MiddleLeft, contentPanel); foreach (var kvp in totalCost.OrderByDescending(x => x.Value)) { var itemDef = ItemManager.FindItemDefinition(kvp.Key); if (itemDef == null) continue; int have = available.TryGetValue(kvp.Key, out var amt) ? amt : 0; int needed = kvp.Value; string itemName = itemDef.displayName?.english ?? itemDef.shortname; bool canAfford = have >= needed; string rowBgColor = canAfford ? "0.15 0.25 0.15 0.5" : "0.25 0.15 0.15 0.5"; string costRow = CreatePanel(ref elements, $"0.05 {costY - costRowHeight}", $"0.95 {costY}", rowBgColor, contentPanel, $"RepairCostRow_{kvp.Key}"); CreateLabel(ref elements, "0.05 0", "0.5 1", "0.9 0.9 0.9 1", "0 0 0 0", itemName, 15, TextAnchor.MiddleLeft, costRow); string amountColor = canAfford ? "0.3 1 0.3 1" : "1 0.7 0.7 1"; string amountText = $"{needed:N0} / {have:N0}"; CreateLabel(ref elements, "0.5 0", "0.95 1", amountColor, "0 0 0 0", amountText, 15, TextAnchor.MiddleRight, costRow); costY -= costSpacing; } } const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closerepair", 15); CreateActionButton(ref elements, contentPanel, "0.05 0.02", "0.48 0.1", actionButtonColor, canRepair ? "Repair All" : "Partial Repair", "cupboardplus.performrepair", 15); CuiHelper.AddUi(player, elements); } private bool CanAffordRepair(Dictionary available, Dictionary totalCost) { foreach (var cost in totalCost) { int needed = cost.Value; int have = available.TryGetValue(cost.Key, out var amt) ? amt : 0; if (have < needed) return false; } return true; } private void CloseRepairUI(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, "RepairMainPanel"); CuiHelper.DestroyUi(player, "RepairContentPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _openPanels.Remove(player.userID); _settingsPanelOpen.Remove(player.userID); StopRandomColorAnimation(player); } private class WallpaperInfo { public int ContentId; public string Name; public string ImageUrl; public string Category; } private void CacheAllWallpapers() { _cachedWallpapersByType = new Dictionary>(); var wallpaperItems = new Dictionary { ["Foundation"] = -551431036, ["Wall"] = 553967074, ["Ceiling"] = 1730664641 }; foreach (var kvp in wallpaperItems) { string blockType = kvp.Key; int itemId = kvp.Value; var wallpapers = new List<(int contentId, string name, int itemId)>(); wallpapers.Add((1, "None", itemId)); wallpapers.Add((0, "Default", itemId)); var itemDef = ItemManager.FindItemDefinition(itemId); if (itemDef != null && itemDef.skins != null) { foreach (var skin in itemDef.skins) { if (skin.id == 0 || skin.id == 1) continue; string skinName = skin.name ?? ""; string displayName = ExtractDisplayNameFromAssetPath(skinName); if (string.IsNullOrEmpty(displayName) || displayName == "Unknown") { displayName = skinName; } wallpapers.Add(((int)skin.id, displayName, itemId)); } } wallpapers = wallpapers.OrderBy(w => w.contentId == 1 ? 0 : (w.contentId == 0 ? 1 : 2)) .ThenBy(w => w.contentId) .ThenBy(w => w.name) .ToList(); _cachedWallpapersByType[blockType] = wallpapers; } } private Dictionary> GetAllWallpapersByType() { if (_cachedWallpapersByType == null) { CacheAllWallpapers(); } var result = new Dictionary>(); foreach (var kvp in _cachedWallpapersByType) { result[kvp.Key] = kvp.Value.Select(w => (w.contentId, w.name)).ToList(); } return result; } private List GetAllWallpapers() { if (_cachedWallpapers != null) { return _cachedWallpapers; } var wallpapers = new List(); var wallpaperByType = GetAllWallpapersByType(); var wallpaperCategories = new Dictionary>(); foreach (var blockType in wallpaperByType.Keys) { foreach (var (contentId, name) in wallpaperByType[blockType]) { if (!wallpaperCategories.ContainsKey(contentId)) { wallpaperCategories[contentId] = new HashSet(); } wallpaperCategories[contentId].Add(blockType); } } foreach (var kvp in wallpaperCategories) { int contentId = kvp.Key; var categories = kvp.Value; string displayName = "Unknown"; foreach (var blockType in wallpaperByType.Keys) { var wallpaper = wallpaperByType[blockType].FirstOrDefault(w => w.contentId == contentId); if (wallpaper.contentId == contentId) { displayName = wallpaper.name; break; } } string category; if (categories.Count == 1) { category = categories.First(); } else { category = "All"; } wallpapers.Add(new WallpaperInfo { ContentId = contentId, Name = displayName, ImageUrl = null, Category = category }); } _cachedWallpapers = wallpapers; return wallpapers; } private string ExtractDisplayNameFromAssetPath(string assetPath) { if (string.IsNullOrEmpty(assetPath)) return "Unknown"; string name = assetPath; if (name.Contains("/")) { name = name.Substring(name.LastIndexOf('/') + 1); } if (name.Contains(".")) { int lastDot = name.LastIndexOf('.'); if (lastDot > 0) { name = name.Substring(0, lastDot); } } name = name.Replace(".skin.asset", ""); name = name.Replace(".asset", ""); name = name.Replace(".skin", ""); name = name.Replace("wallpaper.", ""); name = name.Replace("floorpaper.", ""); name = name.Replace("ceilingpaper.", ""); var parts = name.Split('.'); var displayParts = new List(); foreach (var part in parts) { if (!string.IsNullOrEmpty(part) && part.ToLower() != "skin") { string capitalized = part.Substring(0, 1).ToUpper() + part.Substring(1); displayParts.Add(capitalized); } } string result = string.Join(" ", displayParts); result = result.Replace("Xmas", "Christmas"); result = result.Trim(); if (string.IsNullOrEmpty(result)) result = "Unknown"; return result; } private bool CheckContentOwnership(BasePlayer player, int contentId) { if (player == null) return false; if (contentId == 0 || contentId == 1) return true; if (PlayerDLCAPI != null) { try { object initializedResult = PlayerDLCAPI.Call("Initialized"); bool isInitialized = initializedResult is bool && (bool)initializedResult; if (isInitialized) { object result = PlayerDLCAPI.Call("CheckContentOwnership", player, contentId); if (result is bool && (bool)result) return true; } } catch { } } try { if (player.blueprints != null) { return player.blueprints.CheckSkinOwnership(contentId, player); } } catch { } return false; } private void CheckAndCacheOwnedWallpapers(BasePlayer player) { if (player == null) { _ownedWallpaperCache[0] = new HashSet(); return; } var ownedContentIds = new HashSet(); ownedContentIds.Add(0); ownedContentIds.Add(1); try { var wallpaperByType = GetAllWallpapersByType(); var allWallpaperIds = new HashSet(); foreach (var blockType in wallpaperByType.Keys) { foreach (var (contentId, name) in wallpaperByType[blockType]) { if (contentId > 1) { allWallpaperIds.Add(contentId); } } } foreach (var contentId in allWallpaperIds) { try { bool ownsWallpaper = CheckContentOwnership(player, contentId); if (ownsWallpaper) { ownedContentIds.Add(contentId); } } catch (Exception ex) { } } _ownedWallpaperCache[player.userID] = ownedContentIds; } catch (Exception ex) { _ownedWallpaperCache[player.userID] = new HashSet { 0, 1 }; } } private List GetAvailableWallpapers(string blockType) { return GetAllWallpapers(); } private void OpenWallpaperUI(BasePlayer player) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } CheckAndCacheOwnedWallpapers(player); CuiHelper.DestroyUi(player, "WallpaperMainPanel"); _openPanels[player.userID] = "WallpaperMainPanel"; var elements = new CuiElementContainer(); var panel = CreatePanel(ref elements, "0.3435 0.121", "0.64 0.855", _uiSettings.BackgroundColor, "Overlay", "WallpaperMainPanel", true, true); const string headerImageName = "CupboardPlusHeader"; string headerImagePng = null; string headerImageUrl = null; if (ImageLibrary != null) { headerImagePng = (string)ImageLibrary.Call("GetImage", headerImageName); if (string.IsNullOrEmpty(headerImagePng)) { headerImageUrl = (string)ImageLibrary.Call("GetImage", headerImageName, 0, true); } } if (string.IsNullOrEmpty(headerImagePng) && string.IsNullOrEmpty(headerImageUrl)) { headerImageUrl = _config.HeaderImageUrl; } elements.Add(new CuiElement { Parent = panel, Name = "WallpaperHeaderImage", Components = { new CuiRectTransformComponent { AnchorMin = "0.01 0.84", AnchorMax = "0.99 0.99" }, !string.IsNullOrEmpty(headerImagePng) ? new CuiRawImageComponent { Png = headerImagePng, Color = "1 1 1 1" } : new CuiRawImageComponent { Url = headerImageUrl, Color = "1 1 1 1" } } }); var blockTypes = new[] { "Floor", "Wall", "Ceiling" }; float tabWidth = 0.3133f; float startX = 0.02f; float yMin = 0.75f; float yMax = 0.8f; const string defaultTab = "Floor"; for (int i = 0; i < blockTypes.Length; i++) { var blockType = blockTypes[i]; float xMin = startX + (i * (tabWidth + 0.01f)); float xMax = xMin + tabWidth; const string buttonColor = "1 1 1 0.15"; const string activeTabColor = "0.3 0.5 0.3 0.8"; string tabColor = (blockType == defaultTab) ? activeTabColor : buttonColor; string command = $"cupboardplus.selectwallpapertab {blockType}"; string buttonPanel = CreatePanel(ref elements, $"{xMin} {yMin}", $"{xMax} {yMax}", tabColor, panel, $"WallpaperTab_{blockType}"); CreateLabel(ref elements, "0 0.1", "1 0.9", "1 1 1 1", "0 0 0 0", blockType.ToUpper(), 16, TextAnchor.MiddleCenter, buttonPanel); elements.Add(new CuiButton { Button = { Color = "0 0 0 0", Command = command }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, buttonPanel, $"WallpaperTab_Button_{blockType}"); } bool advancedMode = GetPlayerData(player.userID).WallpaperAdvancedMode; float contentPanelYMax = 0.7401f; if (advancedMode) { contentPanelYMax = 0.67f; _selectedWallpaperTier[player.userID] = BuildingGrade.Enum.Wood; var tierGrades = new[] { BuildingGrade.Enum.Wood, BuildingGrade.Enum.Stone, BuildingGrade.Enum.Metal, BuildingGrade.Enum.TopTier }; float tierTabWidth = 0.23f; float tierStartX = 0.02f; float tierYMin = 0.68f; float tierYMax = 0.73f; for (int i = 0; i < tierGrades.Length; i++) { var grade = tierGrades[i]; string tierName = grade.ToString(); float xMin = tierStartX + (i * (tierTabWidth + 0.01f)); float xMax = xMin + tierTabWidth; const string tierButtonColor = "1 1 1 0.15"; const string tierActiveColor = "0.3 0.5 0.3 0.8"; string tierColor = (grade == BuildingGrade.Enum.Wood) ? tierActiveColor : tierButtonColor; string tierCommand = $"cupboardplus.selectwallpapertiertab {tierName}"; string tierPanelName = $"WallpaperTierTab_{tierName}"; string tierButtonPanel = CreatePanel(ref elements, $"{xMin} {tierYMin}", $"{xMax} {tierYMax}", tierColor, panel, tierPanelName); CreateLabel(ref elements, "0 0.1", "1 0.9", "1 1 1 1", "0 0 0 0", GetTierName(grade).ToUpper(), 14, TextAnchor.MiddleCenter, tierButtonPanel); elements.Add(new CuiButton { Button = { Color = "0 0 0 0", Command = tierCommand }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, tierButtonPanel, $"WallpaperTierTab_Button_{tierName}"); } } else { _selectedWallpaperTier.Remove(player.userID); } string contentPanel = CreatePanel(ref elements, "0 0", $"1 {contentPanelYMax}", "0.15 0.15 0.15 0.8", panel, "WallpaperContentPanel"); const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closewallpaper", 15); if (!_settingsPanelOpen.TryGetValue(player.userID, out var settingsOpen) || !settingsOpen) { CreateSettingsButton(ref elements, player); } CuiHelper.AddUi(player, elements); _selectedWallpaperTab[player.userID] = "Floor"; RefreshWallpaperContentPanel(player, "foundation"); } private void RefreshWallpaperContentPanel(BasePlayer player, string blockType) { var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } var wallpaperByType = GetAllWallpapersByType(); var ownedContentIds = _ownedWallpaperCache.TryGetValue(player.userID, out var owned) ? owned : new HashSet(); string lookupKey = blockType.Equals("foundation", StringComparison.OrdinalIgnoreCase) ? "Foundation" : blockType.Equals("wall", StringComparison.OrdinalIgnoreCase) ? "Wall" : blockType.Equals("ceiling", StringComparison.OrdinalIgnoreCase) ? "Ceiling" : blockType; var wallpapersForType = wallpaperByType.ContainsKey(lookupKey) ? wallpaperByType[lookupKey] : new List<(int, string)>(); var wallpapers = new List(); var seenContentIds = new HashSet(); foreach (var (contentId, name) in wallpapersForType) { if (contentId > 1 && !ownedContentIds.Contains(contentId)) { continue; } if (contentId > 1 && seenContentIds.Contains(contentId)) continue; seenContentIds.Add(contentId); wallpapers.Add(new WallpaperInfo { ContentId = contentId, Name = name, ImageUrl = null, Category = blockType }); } var elements = new CuiElementContainer(); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); CuiHelper.DestroyUi(player, "WallpaperScrollContainer"); CuiHelper.DestroyUi(player, "WallpaperScrollView"); bool advancedMode = GetPlayerData(player.userID).WallpaperAdvancedMode; string contentPanelAnchorMax = advancedMode ? "1 0.67" : "1 0.74"; string contentPanel = CreatePanel(ref elements, "0 0", contentPanelAnchorMax, "0.15 0.15 0.15 0.8", "WallpaperMainPanel", "WallpaperContentPanel"); string scrollContainerName = "WallpaperScrollContainer"; string scrollViewName = "WallpaperScrollView"; string scrollContainer = CreatePanel(ref elements, "0.05 0.12", "0.95 0.95", "0.12 0.12 0.12 0.3", contentPanel, scrollContainerName); int boxesPerRow = 3; int numberOfWallpapers = wallpapers.Count; int numberOfRows = (int)Math.Ceiling(numberOfWallpapers / (double)boxesPerRow); int boxSpacing = 24; int boxHeightPx = 140; int panelSize = -boxSpacing * numberOfRows - boxHeightPx * numberOfRows; elements.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0.12 0.12 0.12 0.3" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = UnityEngine.UI.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" } } }); int wallpaperIndex = 0; foreach (var wallpaper in wallpapers) { int row = wallpaperIndex / boxesPerRow; int col = wallpaperIndex % boxesPerRow; int rowOffset = row * (boxHeightPx + boxSpacing); int offsetMin = -rowOffset - boxHeightPx; int offsetMax = -rowOffset; float boxWidth = 0.3f; float boxSpacingX = 0.05f; float startX = 0f; float xMin = startX + (col * (boxWidth + boxSpacingX)); float xMax = xMin + boxWidth; string boxPanelName = $"WallpaperBox_{wallpaper.ContentId}_{blockType}"; elements.Add(new CuiElement { Name = boxPanelName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = $"{xMin} 0.998", AnchorMax = $"{xMax} 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = "1 1 1 0.15" } } }); if (wallpaper.ContentId == 1) { const string wallpaperNoneImageName = "CupboardPlusWallpaperNone"; string imagePng = ImageLibrary != null ? (string)ImageLibrary.Call("GetImage", wallpaperNoneImageName) : null; if (!string.IsNullOrEmpty(imagePng)) { elements.Add(new CuiElement { Parent = boxPanelName, Name = $"WallpaperBox_Image_{wallpaper.ContentId}", Components = { new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" }, new CuiRawImageComponent { Png = imagePng, Color = "1 1 1 1" } } }); } else { elements.Add(new CuiElement { Parent = boxPanelName, Name = $"WallpaperBox_Image_{wallpaper.ContentId}", Components = { new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" }, new CuiImageComponent { Color = "0.5 0.5 0.5 1" } } }); } } else { string cacheKey = blockType.Equals("foundation", StringComparison.OrdinalIgnoreCase) ? "Foundation" : blockType.Equals("wall", StringComparison.OrdinalIgnoreCase) ? "Wall" : blockType.Equals("ceiling", StringComparison.OrdinalIgnoreCase) ? "Ceiling" : blockType; int itemId = -551431036; if (_cachedWallpapersByType != null && _cachedWallpapersByType.ContainsKey(cacheKey)) { var cachedWallpaper = _cachedWallpapersByType[cacheKey].FirstOrDefault(w => w.contentId == wallpaper.ContentId); if (cachedWallpaper.contentId == wallpaper.ContentId) { itemId = cachedWallpaper.itemId; } else { itemId = cacheKey == "Foundation" ? -551431036 : (cacheKey == "Wall" ? 553967074 : 1730664641); } } else { itemId = cacheKey == "Foundation" ? -551431036 : (cacheKey == "Wall" ? 553967074 : 1730664641); } ulong skinId = (ulong)wallpaper.ContentId; elements.Add(new CuiElement { Parent = boxPanelName, Name = $"WallpaperBox_Image_{wallpaper.ContentId}", Components = { new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" }, new CuiImageComponent { ItemId = itemId, SkinId = skinId } } }); } string applyCommand = advancedMode && _selectedWallpaperTier.TryGetValue(player.userID, out var tier) && tier.HasValue ? $"cupboardplus.applywallpaper {wallpaper.ContentId} {blockType} {tier.Value}" : $"cupboardplus.applywallpaper {wallpaper.ContentId} {blockType}"; elements.Add(new CuiButton { Button = { Command = applyCommand, Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, boxPanelName, $"WallpaperBox_Button_{wallpaper.ContentId}"); wallpaperIndex++; } const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closewallpaper", 15); CuiHelper.AddUi(player, elements); } private void CloseWallpaperUI(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, "WallpaperMainPanel"); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _openPanels.Remove(player.userID); _settingsPanelOpen.Remove(player.userID); StopRandomColorAnimation(player); } private void OpenAuthedUsersUI(BasePlayer player) { var privilege = GetActiveToolCupboard(player); if (privilege == null) return; CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); CuiHelper.DestroyUi(player, "AuthedUsersScrollContainer"); _openPanels[player.userID] = "AuthedUsersMainPanel"; var elements = new CuiElementContainer(); var panel = CreatePanel(ref elements, "0.3435 0.121", "0.64 0.855", _uiSettings.BackgroundColor, "Overlay", "AuthedUsersMainPanel", true, true); const string headerImageName = "CupboardPlusHeader"; string headerImagePng = null; string headerImageUrl = null; if (ImageLibrary != null) { headerImagePng = (string)ImageLibrary.Call("GetImage", headerImageName); if (string.IsNullOrEmpty(headerImagePng)) headerImageUrl = (string)ImageLibrary.Call("GetImage", headerImageName, 0, true); } if (string.IsNullOrEmpty(headerImagePng) && string.IsNullOrEmpty(headerImageUrl)) headerImageUrl = _config.HeaderImageUrl; elements.Add(new CuiElement { Parent = panel, Name = "AuthedUsersHeaderImage", Components = { new CuiRectTransformComponent { AnchorMin = "0.01 0.84", AnchorMax = "0.99 0.99" }, !string.IsNullOrEmpty(headerImagePng) ? new CuiRawImageComponent { Png = headerImagePng, Color = "1 1 1 1" } : new CuiRawImageComponent { Url = headerImageUrl, Color = "1 1 1 1" } } }); string contentPanel = CreatePanel(ref elements, "0 0", "1 0.74", "0.15 0.15 0.15 0.8", panel, "AuthedUsersContentPanel"); if (privilege.authorizedPlayers == null || privilege.authorizedPlayers.Count == 0) { CreateLabel(ref elements, "0.05 0.4", "0.95 0.6", "1 1 1 0.7", "0 0 0 0", "No one is authorized on this cupboard.", 18, TextAnchor.MiddleCenter, contentPanel); } else { string listHeaderPanel = CreatePanel(ref elements, "0.03 0.88", "0.97 0.96", "0.2 0.2 0.25 0.9", contentPanel, "AuthedUsersListHeader"); CreateLabel(ref elements, "0.02 0.1", "0.7 0.9", "0.9 0.9 0.9 1", "0 0 0 0", "Player", 14, TextAnchor.MiddleLeft, listHeaderPanel); CreateLabel(ref elements, "0.78 0.1", "0.98 0.9", "0.9 0.9 0.9 1", "0 0 0 0", "Action", 14, TextAnchor.MiddleCenter, listHeaderPanel); const string scrollContainerName = "AuthedUsersScrollContainer"; const string scrollViewName = "AuthedUsersScrollView"; string scrollContainer = CreatePanel(ref elements, "0.03 0.08", "0.97 0.86", "0.12 0.12 0.12 0.4", contentPanel, scrollContainerName); int count = privilege.authorizedPlayers.Count; int rowHeightPx = 28; int rowGapPx = 6; int panelSize = -count * (rowHeightPx + rowGapPx); elements.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0.12 0.12 0.12 0.3" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = UnityEngine.UI.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" } } }); int rowIndex = 0; foreach (ulong userId in privilege.authorizedPlayers) { string displayName = BasePlayer.FindByID(userId)?.displayName ?? userId.ToString(); string rowColor = (rowIndex % 2 == 0) ? "0.22 0.22 0.22 0.85" : "0.18 0.18 0.18 0.85"; int rowOffset = rowIndex * (rowHeightPx + rowGapPx); int offsetMin = -rowOffset - rowHeightPx; int offsetMax = -rowOffset; string rowPanelName = $"AuthedRow_{userId}"; elements.Add(new CuiElement { Name = rowPanelName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0.02 0.998", AnchorMax = "0.98 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = rowColor } } }); CreateLabel(ref elements, "0.03 0.1", "0.72 0.9", "1 1 1 1", "0 0 0 0", displayName, 15, TextAnchor.MiddleLeft, rowPanelName); CreateActionButton(ref elements, rowPanelName, "0.76 0.1", "0.98 0.9", "0.55 0.2 0.2 0.9", "Remove", $"cupboardplus.removeauth {userId}", 13, $"AuthedRemove_{userId}"); rowIndex++; } } const string actionButtonColor = "1 1 1 0.15"; CreateActionButton(ref elements, contentPanel, "0.52 0.02", "0.95 0.1", actionButtonColor, "✕ Cancel", "cupboardplus.closeauthedusers", 15); if (!_settingsPanelOpen.TryGetValue(player.userID, out var settingsOpen) || !settingsOpen) { CreateSettingsButton(ref elements, player); } CuiHelper.AddUi(player, elements); } private void CloseAuthedUsersUI(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _openPanels.Remove(player.userID); _settingsPanelOpen.Remove(player.userID); } [ConsoleCommand("cupboardplus.selectwallpapertab")] private void CmdSelectWallpaperTab(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasWallpaperPermission(player)) return; if (arg.Args == null || arg.Args.Length == 0) return; string blockType = arg.Args[0]; _selectedWallpaperTab[player.userID] = blockType; string internalBlockType = blockType.Equals("Floor", StringComparison.OrdinalIgnoreCase) ? "foundation" : blockType.ToLower(); RefreshWallpaperContentPanel(player, internalBlockType); UpdateWallpaperTabHighlight(player, blockType); } [ConsoleCommand("cupboardplus.selectwallpapertiertab")] private void CmdSelectWallpaperTierTab(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasWallpaperPermission(player)) return; if (arg.Args == null || arg.Args.Length == 0) return; string tierName = arg.Args[0]; if (!Enum.TryParse(tierName, true, out var grade) || grade < BuildingGrade.Enum.Wood || grade > BuildingGrade.Enum.TopTier) return; _selectedWallpaperTier[player.userID] = grade; UpdateWallpaperTierHighlight(player, grade); if (_selectedWallpaperTab.TryGetValue(player.userID, out var blockType)) { string internalBlockType = blockType.Equals("Floor", StringComparison.OrdinalIgnoreCase) ? "foundation" : blockType.ToLower(); RefreshWallpaperContentPanel(player, internalBlockType); } } private void UpdateWallpaperTierHighlight(BasePlayer player, BuildingGrade.Enum selectedTier) { var tierGrades = new[] { BuildingGrade.Enum.Wood, BuildingGrade.Enum.Stone, BuildingGrade.Enum.Metal, BuildingGrade.Enum.TopTier }; foreach (var grade in tierGrades) { CuiHelper.DestroyUi(player, $"WallpaperTierTab_{grade}"); } var elements = new CuiElementContainer(); float tierTabWidth = 0.23f; float tierStartX = 0.02f; float tierYMin = 0.68f; float tierYMax = 0.73f; const string tierButtonColor = "1 1 1 0.15"; const string tierActiveColor = "0.3 0.5 0.3 0.8"; for (int i = 0; i < tierGrades.Length; i++) { var grade = tierGrades[i]; string tierName = grade.ToString(); float xMin = tierStartX + (i * (tierTabWidth + 0.01f)); float xMax = xMin + tierTabWidth; string tierColor = (grade == selectedTier) ? tierActiveColor : tierButtonColor; string tierCommand = $"cupboardplus.selectwallpapertiertab {tierName}"; string tierPanelName = $"WallpaperTierTab_{tierName}"; string tierButtonPanel = CreatePanel(ref elements, $"{xMin} {tierYMin}", $"{xMax} {tierYMax}", tierColor, "WallpaperMainPanel", tierPanelName); CreateLabel(ref elements, "0 0.1", "1 0.9", "1 1 1 1", "0 0 0 0", GetTierName(grade).ToUpper(), 14, TextAnchor.MiddleCenter, tierButtonPanel); elements.Add(new CuiButton { Button = { Color = "0 0 0 0", Command = tierCommand }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, tierButtonPanel, $"WallpaperTierTab_Button_{tierName}"); } CuiHelper.AddUi(player, elements); } private void UpdateWallpaperTabHighlight(BasePlayer player, string selectedBlockType) { var blockTypes = new[] { "Floor", "Wall", "Ceiling" }; const string buttonColor = "1 1 1 0.15"; const string activeTabColor = "0.3 0.5 0.3 0.8"; foreach (var blockType in blockTypes) { CuiHelper.DestroyUi(player, $"WallpaperTab_{blockType}"); } var elements = new CuiElementContainer(); float tabWidth = 0.3133f; float startX = 0.02f; float yMin = 0.75f; float yMax = 0.8f; for (int i = 0; i < blockTypes.Length; i++) { var blockType = blockTypes[i]; float xMin = startX + (i * (tabWidth + 0.01f)); float xMax = xMin + tabWidth; string tabColor = (blockType == selectedBlockType) ? activeTabColor : buttonColor; string tabPanelName = $"WallpaperTab_{blockType}"; string tabButtonName = $"WallpaperTab_Button_{blockType}"; string buttonPanel = CreatePanel(ref elements, $"{xMin} {yMin}", $"{xMax} {yMax}", tabColor, "WallpaperMainPanel", tabPanelName); CreateLabel(ref elements, "0 0.1", "1 0.9", "1 1 1 1", "0 0 0 0", blockType.ToUpper(), 16, TextAnchor.MiddleCenter, buttonPanel); elements.Add(new CuiButton { Button = { Color = "0 0 0 0", Command = $"cupboardplus.selectwallpapertab {blockType}" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, buttonPanel, tabButtonName); } CuiHelper.AddUi(player, elements); } [ConsoleCommand("cupboardplus.closewallpaper")] private void CmdCloseWallpaper(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseWallpaperUI(player); } [ConsoleCommand("cupboardplus.applywallpaper")] private void CmdApplyWallpaper(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasWallpaperPermission(player)) return; if (arg.Args == null || arg.Args.Length < 2) { return; } if (!int.TryParse(arg.Args[0], out int contentId)) { return; } string blockType = arg.Args[1]; BuildingGrade.Enum? tier = null; if (arg.Args.Length >= 3 && !string.IsNullOrEmpty(arg.Args[2]) && Enum.TryParse(arg.Args[2], true, out var parsedTier) && parsedTier >= BuildingGrade.Enum.Wood && parsedTier <= BuildingGrade.Enum.TopTier) { tier = parsedTier; } var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } ApplyWallpaperToBuilding(player, privilege, contentId, blockType, tier); } private void ApplyWallpaperToBuilding(BasePlayer player, BuildingPrivlidge privilege, int contentId, string blockType, BuildingGrade.Enum? tier = null) { var blocks = GetConnectedBuildingBlocks(privilege, player); if (blocks.Count == 0) { player.ChatMessage(Lang("Error.NoBlocks", player.UserIDString)); return; } if (contentId > 1 && PlayerDLCAPI != null) { try { object initializedResult = PlayerDLCAPI.Call("Initialized"); bool isInitialized = initializedResult is bool && (bool)initializedResult; if (!isInitialized) { player.ChatMessage(Lang("Error.WallpaperOwnershipUnavailable", player.UserIDString)); return; } bool ownsWallpaper = CheckContentOwnership(player, contentId); if (!ownsWallpaper) { player.ChatMessage(Lang("Error.WallpaperNotOwned", player.UserIDString)); return; } } catch (Exception ex) { player.ChatMessage(Lang("Error.WallpaperOwnershipError", player.UserIDString)); return; } } else if (contentId > 1 && PlayerDLCAPI == null) { player.ChatMessage(Lang("Error.WallpaperOwnershipAdmin", player.UserIDString)); return; } var targetBlocks = new List(); string blockTypeLower = blockType.ToLower(); foreach (var block in blocks) { if (block == null || block.IsDestroyed) continue; string blockName = block.ShortPrefabName.ToLower(); bool matches = false; switch (blockTypeLower) { case "foundation": matches = blockName.Contains("foundation"); break; case "wall": matches = blockName.Contains("wall") && (blockName.Contains("window") || !blockName.Contains("wall.frame")); break; case "ceiling": matches = (blockName.Contains("floor") || blockName.Contains("roof")) && !blockName.Contains("foundation"); break; } if (matches) { targetBlocks.Add(block); } } if (blockTypeLower == "foundation") { ulong floorWallpaperID; if (contentId == 1) { floorWallpaperID = 1UL; } else { floorWallpaperID = (ulong)contentId; } foreach (var block in blocks) { if (block == null || block.IsDestroyed) continue; var (_, _, _, isFloorBlock) = GetBlockTypeInfo(block.ShortPrefabName); if (isFloorBlock) { bool isInternal = IsFloorSide1Internal(block); bool needsUpdate = NeedsWallpaperUpdate(block, floorWallpaperID, 1); if (isInternal && needsUpdate) { targetBlocks.Add(block); } } } } if (tier.HasValue) { targetBlocks = targetBlocks.Where(b => b.grade == tier.Value).ToList(); } if (targetBlocks.Count == 0) { string displayName = GetWallpaperBlockTypeDisplayName(blockType); string tierSuffix = tier.HasValue ? $" ({tier.Value})" : ""; player.ChatMessage($"No {displayName}{tierSuffix} blocks found to apply wallpaper."); return; } var wallpaperByType = GetAllWallpapersByType(); string wallpaperName = null; foreach (var bt in wallpaperByType.Keys) { var wp = wallpaperByType[bt].FirstOrDefault(w => w.contentId == contentId); if (wp.contentId == contentId) { wallpaperName = wp.name; break; } } if (contentId == 0 && wallpaperName != "Default") { player.ChatMessage($"Wallpaper \"{wallpaperName}\" is not yet mapped. Please contact an administrator to add the correct ID."); return; } ulong wallpaperID; if (contentId == 1) { wallpaperID = 1UL; } else { wallpaperID = (ulong)contentId; } StartWallpaperQueue(player, privilege, targetBlocks, contentId, blockType, wallpaperID); } [ConsoleCommand("cupboardplus.closeui")] private void CmdCloseUI(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseUpgradeUI(player); } [ConsoleCommand("cupboardplus.closerepair")] private void CmdCloseRepair(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseRepairUI(player); } [ConsoleCommand("cupboardplus.skinui")] private void CmdSkinUI(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasSkinPermission(player)) return; if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "SkinPanel") { CloseSkinUI(player); return; } if (openPanel == "UpgradeMainPanel") { CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "ContentPanel"); _selectedTiers.Remove(player.userID); } if (openPanel == "RepairMainPanel") { CuiHelper.DestroyUi(player, "RepairMainPanel"); CuiHelper.DestroyUi(player, "RepairContentPanel"); } if (openPanel == "WallpaperMainPanel") { CuiHelper.DestroyUi(player, "WallpaperMainPanel"); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); } if (openPanel == "AuthedUsersMainPanel") { CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); } CuiHelper.DestroyUi(player, "SettingsButton"); OpenSkinUI(player); } [ConsoleCommand("cupboardplus.authedusersui")] private void CmdAuthedUsersUI(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player)) return; if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "AuthedUsersMainPanel") { CloseAuthedUsersUI(player); return; } if (openPanel == "UpgradeMainPanel") { CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "ContentPanel"); _selectedTiers.Remove(player.userID); } if (openPanel == "RepairMainPanel") { CuiHelper.DestroyUi(player, "RepairMainPanel"); CuiHelper.DestroyUi(player, "RepairContentPanel"); } if (openPanel == "WallpaperMainPanel") { CuiHelper.DestroyUi(player, "WallpaperMainPanel"); CuiHelper.DestroyUi(player, "WallpaperContentPanel"); } if (openPanel == "SkinPanel") { CuiHelper.DestroyUi(player, "SkinPanel"); CuiHelper.DestroyUi(player, "SkinContentPanel"); _skinSelectedTier.Remove(player.userID); } CuiHelper.DestroyUi(player, "SettingsButton"); OpenAuthedUsersUI(player); } [ConsoleCommand("cupboardplus.closeauthedusers")] private void CmdCloseAuthedUsers(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseAuthedUsersUI(player); } [ConsoleCommand("cupboardplus.removeauth")] private void CmdRemoveAuth(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player)) return; if (arg.Args == null || arg.Args.Length < 1 || !ulong.TryParse(arg.Args[0], out ulong targetUserId)) return; var privilege = GetActiveToolCupboard(player); if (privilege == null || privilege.authorizedPlayers == null) return; if (!privilege.authorizedPlayers.Remove(targetUserId)) return; privilege.SendNetworkUpdate(BasePlayer.NetworkQueue.Update); OpenAuthedUsersUI(player); } [ConsoleCommand("cupboardplus.selectskintab")] private void CmdSelectSkinTab(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasSkinPermission(player)) return; if (arg.Args == null || arg.Args.Length < 1) return; if (!Enum.TryParse(arg.Args[0], true, out BuildingGrade.Enum tier)) return; var privilege = GetActiveToolCupboard(player); if (privilege == null) return; var blocks = GetConnectedBuildingBlocks(privilege, player); RefreshSkinContentPanel(player, blocks, tier); } [ConsoleCommand("cupboardplus.setdefaultskin")] private void CmdSetDefaultSkin(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasSkinPermission(player)) return; if (arg.Args == null || arg.Args.Length < 1 || !int.TryParse(arg.Args[0], out int contentId)) return; if (!_skinSelectedTier.TryGetValue(player.userID, out var tier)) return; var tierSkins = GetAllTierSkins().Where(s => s.Tier == tier).ToList(); if (!tierSkins.Any(s => s.ContentId == contentId)) return; var playerData = GetPlayerData(player.userID); string tierKey = tier.ToString(); playerData.DefaultSkinPerTier[tierKey] = contentId; SaveData(); UpdateDefaultSkinCheckmarks(player, contentId); } [ConsoleCommand("cupboardplus.applyskin")] private void CmdApplySkin(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasSkinPermission(player)) return; if (arg.Args == null || arg.Args.Length < 1) { player.ChatMessage("Invalid skin ID."); return; } if (!int.TryParse(arg.Args[0], out int contentId)) { player.ChatMessage("Invalid skin ID format."); return; } var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } BuildingGrade.Enum? selectedTier = null; if (_skinSelectedTier.TryGetValue(player.userID, out var tier)) { selectedTier = tier; } ApplySkinToBuilding(player, privilege, contentId, selectedTier); } [ConsoleCommand("cupboardplus.applycontainercolor")] private void CmdApplyContainerColor(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasSkinPermission(player)) return; if (arg.Args == null || arg.Args.Length < 1) { return; } if (!int.TryParse(arg.Args[0], out int contentId) || contentId != 10221) { return; } int colorIndex = -1; if (_selectedColorIndex.TryGetValue(player.userID, out var selectedIdx)) { colorIndex = selectedIdx; } else { var playerData = GetPlayerData(player.userID); if (playerData.SelectedColorIndex >= 0) { colorIndex = playerData.SelectedColorIndex; } } if (colorIndex < 0 || colorIndex >= ColorPalette.Length) { player.ChatMessage("Please select a color in the settings panel first."); return; } var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } var blocks = GetConnectedBuildingBlocks(privilege, player); var containerBlocks = blocks.Where(b => b != null && !b.IsDestroyed && b.grade == BuildingGrade.Enum.Metal && b.skinID == 10221 ).ToList(); if (containerBlocks.Count == 0) { player.ChatMessage("No container pieces found."); return; } var eligibleBlocks = new List(); int skippedCount = 0; foreach (var block in containerBlocks) { if (block == null || block.IsDestroyed) continue; int currentColorIndex = GetBlockColorIndex(block); if (currentColorIndex == colorIndex) { skippedCount++; continue; } eligibleBlocks.Add(block); } if (eligibleBlocks.Count == 0) { player.ChatMessage("All container pieces already have this color."); return; } CloseSkinUI(player); CuiHelper.DestroyUi(player, "SettingsPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); _settingsPanelOpen.Remove(player.userID); StartColorQueue(player, privilege, eligibleBlocks, colorIndex); } private int GetRandomColor() { return UnityEngine.Random.Range(1, ColorPalette.Length); } private int GetBlockColorIndex(BuildingBlock block) { if (block == null || block.IsDestroyed) return -1; try { return (int)block.customColour; } catch (Exception ex) { PrintError($"Error in GetBlockColorIndex: {ex.Message}"); return -1; } } [ConsoleCommand("cupboardplus.closeskin")] private void CmdCloseSkin(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseSkinUI(player); } [ConsoleCommand("cupboardplus.wallpaper")] private void CmdWallpaper(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player) || !HasWallpaperPermission(player)) return; bool canVerifyWallpaperOwnership = false; if (PlayerDLCAPI != null) { try { object initializedResult = PlayerDLCAPI.Call("Initialized"); canVerifyWallpaperOwnership = initializedResult is bool && (bool)initializedResult; } catch { } } if (!canVerifyWallpaperOwnership) { player.ChatMessage(Lang("Error.WallpaperOwnershipAdmin", player.UserIDString)); return; } if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "WallpaperMainPanel") { CloseWallpaperUI(player); return; } if (openPanel == "RepairMainPanel") { CuiHelper.DestroyUi(player, "RepairMainPanel"); CuiHelper.DestroyUi(player, "RepairContentPanel"); } if (openPanel == "UpgradeMainPanel") { CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "ContentPanel"); _selectedTiers.Remove(player.userID); } if (openPanel == "SkinPanel") { CuiHelper.DestroyUi(player, "SkinPanel"); CuiHelper.DestroyUi(player, "SkinContentPanel"); _skinSelectedTier.Remove(player.userID); } if (openPanel == "AuthedUsersMainPanel") { CuiHelper.DestroyUi(player, "AuthedUsersMainPanel"); CuiHelper.DestroyUi(player, "AuthedUsersContentPanel"); } CuiHelper.DestroyUi(player, "SettingsButton"); OpenWallpaperUI(player); } [ConsoleCommand("cupboardplus.opensettings")] private void CmdOpenSettings(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player)) return; OpenSettingsPanel(player); } [ConsoleCommand("cupboardplus.closesettings")] private void CmdCloseSettings(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CuiHelper.DestroyUi(player, "SettingsPanel"); _settingsPanelOpen.Remove(player.userID); StopRandomColorAnimation(player); if (_openPanels.ContainsKey(player.userID)) { var elements = new CuiElementContainer(); CreateSettingsButton(ref elements, player); CuiHelper.AddUi(player, elements); } else { CuiHelper.DestroyUi(player, "SettingsButton"); } } [ConsoleCommand("cupboardplus.toggleupgradeeffect")] private void CmdToggleUpgradeEffect(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player)) return; if (!_openPanels.ContainsKey(player.userID)) { CuiHelper.DestroyUi(player, "SettingsPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); _settingsPanelOpen.Remove(player.userID); return; } bool currentState = _skinUpgradeEffectEnabled.TryGetValue(player.userID, out var enabled) && enabled; bool newState = !currentState; _skinUpgradeEffectEnabled[player.userID] = newState; var playerData = GetPlayerData(player.userID); playerData.UpgradeEffectEnabled = newState; SaveData(); if (_activeQueues.TryGetValue(player.userID, out var upgradeQueue)) { upgradeQueue.UpgradeEffectEnabled = newState; } if (_settingsPanelOpen.ContainsKey(player.userID) && _settingsPanelOpen[player.userID]) { OpenSettingsPanel(player); } } [ConsoleCommand("cupboardplus.togglewallpaperadvanced")] private void CmdToggleWallpaperAdvanced(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player)) return; var playerData = GetPlayerData(player.userID); playerData.WallpaperAdvancedMode = !playerData.WallpaperAdvancedMode; SaveData(); if (_settingsPanelOpen.ContainsKey(player.userID) && _settingsPanelOpen[player.userID]) { OpenSettingsPanel(player); } if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "WallpaperMainPanel") { OpenWallpaperUI(player); } } [ConsoleCommand("cupboardplus.selectcolor")] private void CmdSelectColor(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !HasPermission(player)) return; if (!_openPanels.ContainsKey(player.userID)) { return; } if (arg.Args == null || arg.Args.Length == 0) { return; } if (!int.TryParse(arg.Args[0], out int colorIndex) || colorIndex < 0 || colorIndex >= ColorPalette.Length) { return; } _selectedColorIndex[player.userID] = colorIndex; var playerData = GetPlayerData(player.userID); playerData.SelectedColorIndex = colorIndex; SaveData(); if (colorIndex == 0) { if (!_randomColorTimers.ContainsKey(player.userID)) { StartRandomColorAnimation(player); } } else { bool settingsNeedsIt = _settingsPanelOpen.ContainsKey(player.userID) && _settingsPanelOpen[player.userID]; bool skinNeedsIt = _openPanels.ContainsKey(player.userID) && _openPanels[player.userID] == "SkinPanel"; if (!settingsNeedsIt && !skinNeedsIt) { StopRandomColorAnimation(player); } } if (_openPanels.ContainsKey(player.userID) && _settingsPanelOpen.ContainsKey(player.userID) && _settingsPanelOpen[player.userID]) { UpdateSettingsCloseButtonColor(player); } if (_openPanels.TryGetValue(player.userID, out var openPanel) && openPanel == "SkinPanel") { var privilege = GetActiveToolCupboard(player); if (privilege != null) { var blocks = GetConnectedBuildingBlocks(privilege, player); if (_skinSelectedTier.TryGetValue(player.userID, out var tier) && tier.HasValue) { RefreshSkinContentPanel(player, blocks, tier.Value); } } } } private string GetColorString(int colorIndex) { if (ColorPalette == null || colorIndex < 0 || colorIndex >= ColorPalette.Length) { return "0.867 0.867 0.867"; } return ColorPalette[colorIndex]; } private string GetWallpaperDlcPack(int contentId) { var dlcPacks = new Dictionary> { ["WallpaperStarterPack"] = new HashSet { }, ["FloorCeilingPack"] = new HashSet { 10360, 10361, 10362, 10366, 10367, 10368, 10369, 10370, 10371, 10372, 10375, 10377, 10378, 10379, 10383, 10384, 10385, 10386, 10388, 10389, 10390, 10391, 10392, 10393, 10394, 10395, 10396, 10397, 10398, 10399, 10400, 10401, 10402, 10403, 10404, 10405, 10406, 10407, 10409, 10410, 10412, 10413, 10414, 10415, 10422, 10423, 10425, 10388, 10390, 10391, 10392, 10393, 10394, 10395, 10396, 10398, 10399, 10400, 10401, 10402, 10403, 10404, 10405, 10406, 10407, 10410, 10413, 10414, 10425, 10388, 10389, 10390, 10391, 10392, 10393, 10394, 10395, 10396, 10397, 10398, 10400, 10401, 10402, 10403, 10404, 10405, 10406, 10407, 10409, 10412, 10422 }, ["ExhibitDecorPack"] = new HashSet { 10408, }, ["TwitchRivals"] = new HashSet { 10426, 10427 } }; foreach (var dlcPack in dlcPacks) { if (dlcPack.Value.Contains(contentId)) { return dlcPack.Key; } } if (contentId == 0 || contentId == 1) { return "Free"; } return "Unknown"; } [ChatCommand("cplus")] private void CmdCplusChat(BasePlayer player, string command, string[] args) { ProcessCplusCommand(player, args ?? new string[0]); } [ConsoleCommand("cupboardplus.speed")] private void CmdSetSpeed(ConsoleSystem.Arg arg) { ProcessCplusCommand(arg.Player(), arg.Args ?? new string[0]); } private void ProcessCplusCommand(BasePlayer player, string[] args) { if (player != null && !permission.UserHasPermission(player.UserIDString, PermissionAdmin)) { player.ChatMessage("You don't have permission to use this command."); return; } if (player != null && (args == null || args.Length == 0)) { ShowBuCommandHelp(player); return; } if (args != null && args.Length > 0) { string subCommand = args[0].ToLower(); if (subCommand == "downgrade") { _config.EnableDowngrade = !_config.EnableDowngrade; SaveConfig(); string status = _config.EnableDowngrade ? "enabled" : "disabled"; if (player != null) player.ChatMessage($"Downgrade has been {status}."); return; } if (subCommand == "ui") { if (args.Length < 2) { if (player != null) player.ChatMessage($"Current UI Placement: {_uiSettings.UIPlacement}\nUsage: /cplus ui <1-3>"); return; } if (!int.TryParse(args[1], out int placement) || placement < 1 || placement > 3) { if (player != null) player.ChatMessage("Invalid UI placement. Use a number between 1 and 3."); return; } _uiSettings.UIPlacement = placement; _config.UISettings.UIPlacement = placement; SaveConfig(); string placementDesc = placement == 1 ? "Text only, same location" : placement == 2 ? "Icons, same location" : "Text only, top right"; if (player != null) player.ChatMessage($"UI Placement set to {placement} ({placementDesc})."); return; } } string speedArg = null; if (args != null && args.Length > 0) { if (args[0].ToLower() == "speed" && args.Length > 1) speedArg = args[1]; else if (args[0].ToLower() != "speed") speedArg = args[0]; } if (speedArg == null) { if (player != null) { player.ChatMessage($"Current speed: {GetSpeedDisplay(_config.UpgradeIntervalSeconds)}"); player.ChatMessage("Usage: /cplus speed \n0 = instant | 1 = 1 per second | 2 = 1 per 2 seconds"); } return; } if (!float.TryParse(speedArg, out float speed) || speed < 0) { if (player != null) player.ChatMessage(Lang("Command.Bu.SpeedInvalidRange", player.UserIDString)); return; } _config.UpgradeIntervalSeconds = speed; SaveConfig(); string speedDisplay = GetSpeedDisplay(speed); string message = Lang("Command.Bu.SpeedSetTo", player?.UserIDString, speedDisplay); if (player != null) player.ChatMessage(message); } #endregion #region Helper Methods private HashSet GetAuthedUserIds(BuildingPrivlidge privilege) { var set = new HashSet(); if (privilege?.authorizedPlayers == null) return set; foreach (var id in privilege.authorizedPlayers) set.Add(id); return set; } private ulong GetBlockOwnerId(BuildingBlock block) { if (block == null) return 0; ulong netId = block.net?.ID.Value ?? 0UL; return netId != 0 && _blockBuilderCache.TryGetValue(netId, out ulong ownerId) ? ownerId : 0; } private bool IsBlockOwnerAuthedOnTC(BuildingBlock block, BuildingPrivlidge privilege) { ulong ownerId = GetBlockOwnerId(block); if (ownerId == 0) return true; var authedIds = GetAuthedUserIds(privilege); return authedIds.Contains(ownerId); } private bool ValidateQueueBlock(BuildingBlock block, BuildingPrivlidge privilege, BasePlayer player) { if (block == null || block.IsDestroyed) return false; var blockPrivilege = block.GetBuildingPrivilege(); if (blockPrivilege != privilege) return false; if (!privilege.CanBuild(player) || !privilege.IsAuthed(player)) return false; if (!IsBlockOwnerAuthedOnTC(block, privilege)) return false; return true; } private bool CheckResources(BuildingPrivlidge privilege, List costs, bool noCost) { if (noCost) return true; var available = GetAvailableResources(privilege); foreach (var itemCost in costs) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; int have = available.TryGetValue(itemId, out var amt) ? amt : 0; if (have < need) return false; } return true; } private bool ConsumeResources(BuildingPrivlidge privilege, List costs, bool noCost) { if (noCost) return true; if (!CheckResources(privilege, costs, false)) return false; foreach (var itemCost in costs) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; var items = privilege.inventory.itemList.Where(i => i.info.itemid == itemId).ToList(); foreach (var item in items) { if (need <= 0) break; int remove = Math.Min(item.amount, need); item.UseItem(remove); need -= remove; } } return true; } private bool CheckResourceAmount(BuildingPrivlidge privilege, int itemId, int amount, bool noCost) { if (noCost) return true; var available = GetAvailableResources(privilege); int have = available.TryGetValue(itemId, out var amt) ? amt : 0; return have >= amount; } private void GiveResourceAmount(BuildingPrivlidge privilege, int itemId, int amount) { if (privilege?.inventory == null || amount <= 0) return; var itemDef = ItemManager.FindItemDefinition(itemId); if (itemDef == null) return; var existingItems = privilege.inventory.itemList.Where(i => i.info.itemid == itemId && i.amount < i.MaxStackable()).ToList(); int remaining = amount; foreach (var item in existingItems) { if (remaining <= 0) break; int spaceAvailable = item.MaxStackable() - item.amount; int toAdd = Mathf.Min(remaining, spaceAvailable); item.amount += toAdd; remaining -= toAdd; } while (remaining > 0) { int stackSize = Mathf.Min(remaining, itemDef.stackable); var newItem = ItemManager.Create(itemDef, stackSize); if (newItem != null && !newItem.MoveToContainer(privilege.inventory)) { newItem.Remove(); break; } remaining -= stackSize; } } private bool ConsumeResourceAmount(BuildingPrivlidge privilege, int itemId, int amount, bool noCost) { if (noCost) return true; if (!CheckResourceAmount(privilege, itemId, amount, false)) return false; var items = privilege.inventory.itemList.Where(i => i.info.itemid == itemId).ToList(); int need = amount; foreach (var item in items) { if (need <= 0) break; int remove = Math.Min(item.amount, need); item.UseItem(remove); need -= remove; } return true; } private bool HasPermission(BasePlayer player) => HasPermission(player, PermissionUse); private bool HasUpgradePermission(BasePlayer player) => HasPermission(player, PermissionUpgrade); private bool HasSkinPermission(BasePlayer player) => HasPermission(player, PermissionSkin); private bool HasRepairPermission(BasePlayer player) => HasPermission(player, PermissionRepair); private bool HasWallpaperPermission(BasePlayer player) => HasPermission(player, PermissionWallpaper); private bool HasDowngradePermission(BasePlayer player) => HasPermission(player, PermissionDowngrade); private bool HasNoCostPermission(BasePlayer player) => permission.UserHasPermission(player.UserIDString, PermissionNoCost); private bool HasPermission(BasePlayer player, string perm) => permission.UserHasPermission(player.UserIDString, perm) || permission.UserHasPermission(player.UserIDString, PermissionAdmin); private void DestroyUi(BasePlayer player) { CuiHelper.DestroyUi(player, "UpgradeMainPanel"); CuiHelper.DestroyUi(player, "UpgradePreviewPanel"); CuiHelper.DestroyUi(player, "ContentPanel"); CuiHelper.DestroyUi(player, "SettingsButton"); CuiHelper.DestroyUi(player, "SettingsPanel"); _uiViewers.Remove(player.userID); _selectedTiers.Remove(player.userID); _settingsPanelOpen.Remove(player.userID); } private bool IsUnderAttack(BasePlayer player) { if (NoEscape == null) return false; object result = NoEscape.Call("IsRaidBlocked", player); return result is bool && (bool)result; } private string GetTierName(BuildingGrade.Enum grade) { switch (grade) { case BuildingGrade.Enum.Wood: return "Wood"; case BuildingGrade.Enum.Stone: return "Stone"; case BuildingGrade.Enum.Metal: return "Metal"; case BuildingGrade.Enum.TopTier: return "Armored"; default: return "Unknown"; } } private string GetSkinName(int contentId, BuildingGrade.Enum? tier = null) { var allSkins = GetAllTierSkins(); if (tier.HasValue) { var skin = allSkins.FirstOrDefault(s => s.ContentId == contentId && s.Tier == tier.Value); if (skin != null) return skin.SkinName; return GetTierName(tier.Value); } var fallbackSkin = allSkins.FirstOrDefault(s => s.ContentId == contentId); if (fallbackSkin != null) return fallbackSkin.SkinName; return "Default"; } private void ShowBuCommandHelp(BasePlayer player) { string currentSpeed = GetSpeedDisplay(_config.UpgradeIntervalSeconds); string enabled = Lang("UI.Enabled", player.UserIDString); string disabled = Lang("UI.Disabled", player.UserIDString); string placementDesc = _uiSettings.UIPlacement == 1 ? "Text only, same location" : _uiSettings.UIPlacement == 2 ? "Icons, same location" : "Text only, top right"; string helpMessage = $"{Lang("Command.Bu.HelpHeader", player.UserIDString)}\n\n" + $"Commands:\n" + $"/cplus speed - Set upgrade speed (0=instant, 1=1/sec, 2=1/2sec)\n" + $"/cplus downgrade - Toggle downgrade feature\n" + $"/cplus ui <1-3> - Set UI placement\n\n" + $"Settings:\n" + $"Speed: {currentSpeed}\n" + $"Animation: {(_config.EnableUpgradeAnimation ? enabled : disabled)}\n" + $"Downgrade: {(_config.EnableDowngrade ? enabled : disabled)}\n" + $"UI Placement: {_uiSettings.UIPlacement} ({placementDesc})"; player.ChatMessage(helpMessage); } private string GetSpeedDisplay(float seconds) { if (seconds == 0) return "INSTANT"; if (seconds == 1) return "1 per second"; if (seconds < 1) return $"{1f / seconds:0.##} per second"; return $"1 every {seconds:0.##} seconds"; } #endregion #region Upgrade Logic private BuildingPrivlidge GetActiveToolCupboard(BasePlayer player) { if (_activeToolCupboards.TryGetValue(player.userID, out var tc)) { if (tc != null && !tc.IsDestroyed && tc.IsAuthed(player)) { return tc; } else { _activeToolCupboards.Remove(player.userID); } } return null; } private List GetConnectedBuildingBlocks(BuildingPrivlidge privilege, BasePlayer player = null) { var buildingBlocks = new List(); if (privilege == null) return buildingBlocks; var building = privilege.GetBuilding(); if (building == null) return buildingBlocks; var allBlocks = building.buildingBlocks; if (allBlocks == null) return buildingBlocks; foreach (var block in allBlocks) { if (block == null || block.IsDestroyed) continue; if (block.buildingID != privilege.buildingID) continue; var blockPrivilege = block.GetBuildingPrivilege(); if (blockPrivilege != privilege) { continue; } if (player != null) { if (!privilege.IsAuthed(player)) continue; if (!privilege.CanBuild(player)) continue; } buildingBlocks.Add(block); } return buildingBlocks; } private bool IsWallSideInternal(BuildingBlock block, int side) { if (block == null || block.IsDestroyed) return false; string blockName = block.ShortPrefabName.ToLower(); bool isWall = blockName.Contains("wall") && !blockName.Contains("window"); if (isWall) { ulong blockId = block.net?.ID.Value ?? 0; ulong cacheKey = blockId * 10 + (ulong)side; if (_wallpaperInternalCache.TryGetValue(cacheKey, out bool cached)) { return cached; } bool result = IsWallSideInternalHardside(block); _wallpaperInternalCache[cacheKey] = result; if (_wallpaperInternalCache.Count > 10000) { _wallpaperInternalCache.Clear(); } return result; } var building = BuildingManager.server.GetBuilding(block.buildingID); if (building == null || building.buildingBlocks == null) return false; Vector3 blockCenter = block.transform.position; Quaternion blockRotation = block.transform.rotation; Vector3 forward = blockRotation * Vector3.forward; Vector3 sideDirection = (side == 0) ? forward : -forward; float startOffset = 0.1f; Vector3 rayStart = blockCenter + (sideDirection * startOffset); float rayDistance = 18.0f; RaycastHit hit; if (Physics.Raycast(rayStart, sideDirection, out hit, rayDistance, LayerMask.GetMask("Construction"))) { var hitBlock = hit.collider.GetComponent(); if (hitBlock != null && !hitBlock.IsDestroyed && hitBlock.buildingID == block.buildingID) { string hitBlockName = hitBlock.ShortPrefabName.ToLower(); bool isWallDoorOrWindow = hitBlockName.Contains("wall") || hitBlockName.Contains("door") || hitBlockName.Contains("window") || hitBlockName.Contains("gate"); if (isWallDoorOrWindow) { Vector3 hitPoint = hit.point; Vector3 checkAbove = hitPoint + Vector3.up * 0.5f; RaycastHit hitUp; if (Physics.Raycast(checkAbove, Vector3.up, out hitUp, 5.0f, LayerMask.GetMask("Construction"))) { var ceilingBlock = hitUp.collider.GetComponent(); if (ceilingBlock != null && !ceilingBlock.IsDestroyed && ceilingBlock.buildingID == block.buildingID) { string ceilingName = ceilingBlock.ShortPrefabName.ToLower(); bool isCeilingOrFloor = ceilingName.Contains("floor") || ceilingName.Contains("roof") || ceilingName.Contains("ceiling"); if (isCeilingOrFloor) { return true; } } } RaycastHit hitDown; if (Physics.Raycast(checkAbove, Vector3.down, out hitDown, 5.0f, LayerMask.GetMask("Construction"))) { var floorBlock = hitDown.collider.GetComponent(); if (floorBlock != null && !floorBlock.IsDestroyed && floorBlock.buildingID == block.buildingID) { string floorName = floorBlock.ShortPrefabName.ToLower(); bool isFloor = floorName.Contains("floor") || floorName.Contains("foundation"); if (isFloor) { return true; } } } } } } return false; } private bool IsWallTypeBlock(BuildingBlock block) { if (block == null || block.IsDestroyed) return false; string name = block.ShortPrefabName.ToLower(); return name.Contains("wall") || name.Contains("door") || name.Contains("window") || name.Contains("gate"); } private bool IsFloorTypeBlock(BuildingBlock block) { if (block == null || block.IsDestroyed) return false; string name = block.ShortPrefabName.ToLower(); return name.Contains("foundation") || name.Contains("floor") || name.Contains("roof") || name.Contains("ceiling"); } private (bool isFoundation, bool isFloor, bool isRoofOrCeiling, bool isFloorBlock) GetBlockTypeInfo(string blockName) { blockName = blockName.ToLower(); bool isFoundation = blockName.Contains("foundation"); bool isFloor = blockName.Contains("floor") && !isFoundation; bool isRoofOrCeiling = blockName.Contains("roof") || blockName.Contains("ceiling"); bool isFloorBlock = isFloor && !isRoofOrCeiling; return (isFoundation, isFloor, isRoofOrCeiling, isFloorBlock); } private bool IsFloorSide1Internal(BuildingBlock floor) { if (floor == null || floor.IsDestroyed) return false; var building = BuildingManager.server.GetBuilding(floor.buildingID); if (building == null || building.buildingBlocks == null) return false; string blockName = floor.ShortPrefabName.ToLower(); if (blockName.Contains("foundation")) return false; bool isFloor = blockName.Contains("floor") || blockName.Contains("roof") || blockName.Contains("ceiling"); if (!isFloor) return false; Vector3 floorCenter = floor.transform.position; Bounds worldBounds = floor.bounds; float floorTopY = worldBounds.max.y; if (worldBounds.center.sqrMagnitude < 0.01f) { var renderer = floor.GetComponent(); if (renderer != null) { worldBounds = renderer.bounds; floorTopY = worldBounds.max.y; } else { floorTopY = floorCenter.y + 0.1f; } } if (floorTopY < floorCenter.y) floorTopY = floorCenter.y + 0.1f; float rayStartX = floorCenter.x; float rayStartZ = floorCenter.z; if (blockName.Contains("triangle")) { Vector3 floorForward = floor.transform.forward; rayStartX += floorForward.x * 0.6f; rayStartZ += floorForward.z * 0.6f; } Vector3 rayStart = new Vector3(rayStartX, floorTopY + 0.1f, rayStartZ); float rayDistance = 5.0f; RaycastHit hitUp; if (Physics.Raycast(rayStart, Vector3.up, out hitUp, rayDistance, LayerMask.GetMask("Construction"))) { var ceilingBlock = hitUp.GetEntity() as BuildingBlock; if (ceilingBlock != null && !ceilingBlock.IsDestroyed && ceilingBlock != floor && ceilingBlock.buildingID == floor.buildingID) { Bounds ceilingBounds = ceilingBlock.bounds; float ceilingBottom = ceilingBounds.min.y; Vector3 ceilingTransformPos = ceilingBlock.transform.position; bool boundsInvalid = ceilingBounds.center.sqrMagnitude < 0.01f || ceilingBottom < ceilingTransformPos.y - 1.0f || (ceilingTransformPos.y > 1.0f && ceilingBottom < 0.5f); if (boundsInvalid) { var ceilingRenderer = ceilingBlock.GetComponent(); if (ceilingRenderer != null) { ceilingBounds = ceilingRenderer.bounds; ceilingBottom = ceilingBounds.min.y; if (ceilingBottom < ceilingTransformPos.y - 1.0f || ceilingBounds.center.sqrMagnitude < 0.01f) { ceilingBottom = ceilingTransformPos.y - 0.1f; } } else { ceilingBottom = ceilingTransformPos.y - 0.1f; } } if (ceilingBottom <= floorTopY + 0.01f) { ceilingBottom = hitUp.point.y; } string ceilingName = ceilingBlock.ShortPrefabName.ToLower(); bool isFloorOrRoof = ceilingName.Contains("floor") || ceilingName.Contains("roof") || ceilingName.Contains("ceiling"); if (isFloorOrRoof) { if (ceilingBottom > floorTopY + 0.01f) { return true; } else { if (hitUp.point.y > floorTopY + 0.01f) { return true; } } } } } return false; } private bool RaycastWithFallback(Vector3 origin, Vector3 direction, float distance, out RaycastHit hit) { if (Physics.Raycast(origin, direction, out hit, distance, LayerMask.GetMask("Construction"))) return true; return Physics.Raycast(origin, direction, out hit, distance); } private bool CheckFloorBlock(RaycastHit hit, BuildingBlock excludeBlock) { var block = hit.GetEntity() as BuildingBlock; return block != null && block != excludeBlock && !block.IsDestroyed && block.buildingID == excludeBlock.buildingID && IsFloorTypeBlock(block); } private const float WallInternalRayForwardLength = 18.0f; private const float WallInternalRayUpDownLength = 10.0f; private const float WallInternalHeightBelowTop = 0.5f; private const float WallInternalHeightBelowTopLowWall = 0.25f; private const float WallInternalLowWallHeightThreshold = 1.2f; private bool TryGetWallInternalRays(BuildingBlock block, out Vector3 rayStartPos, out Vector3 forwardDir, out Vector3 upDir) { rayStartPos = default; forwardDir = default; upDir = default; if (block == null || block.IsDestroyed) return false; var building = BuildingManager.server.GetBuilding(block.buildingID); if (building == null || building.buildingBlocks == null) return false; Vector3 blockCenter = block.transform.position; Quaternion blockRotation = block.transform.rotation; Vector3 wallLocalRight = blockRotation * Vector3.right; Vector3 wallLocalUp = blockRotation * Vector3.up; Vector3 wallLocalForward = blockRotation * Vector3.forward; Bounds wallBounds = block.bounds; float localHeight = wallBounds.max.y - wallBounds.min.y; float heightBelowTop = localHeight <= WallInternalLowWallHeightThreshold ? WallInternalHeightBelowTopLowWall : WallInternalHeightBelowTop; float offsetFromCenter = Mathf.Clamp(localHeight * 0.5f - heightBelowTop, -localHeight * 0.5f + 0.01f, localHeight * 0.5f - 0.01f); rayStartPos = blockCenter + (wallLocalRight * 1f) + (wallLocalForward * 1f) + (wallLocalUp * offsetFromCenter); forwardDir = wallLocalRight; upDir = wallLocalUp; return true; } private bool IsWallSideInternalHardside(BuildingBlock block) { if (!TryGetWallInternalRays(block, out Vector3 rayStartPos, out Vector3 forwardDir, out Vector3 upDir)) return false; if (!RaycastWithFallback(rayStartPos, forwardDir, WallInternalRayForwardLength, out RaycastHit hit)) return false; var hitBlock = hit.GetEntity() as BuildingBlock; if (hitBlock == null || hitBlock.IsDestroyed || hitBlock.buildingID != block.buildingID || !IsWallTypeBlock(hitBlock)) return false; bool foundAbove = RaycastWithFallback(rayStartPos, upDir, WallInternalRayUpDownLength, out RaycastHit hitUp) && CheckFloorBlock(hitUp, block); bool foundBelow = RaycastWithFallback(rayStartPos, -upDir, WallInternalRayUpDownLength, out RaycastHit hitDown) && CheckFloorBlock(hitDown, block); return foundAbove && foundBelow; } private bool CanUpgradeToTier(BuildingGrade.Enum currentGrade, BuildingGrade.Enum targetGrade) { if (targetGrade <= currentGrade) { return _config.EnableDowngrade && targetGrade < currentGrade; } if (targetGrade == BuildingGrade.Enum.Twigs) return false; if (currentGrade == BuildingGrade.Enum.Twigs) return targetGrade == BuildingGrade.Enum.Wood || targetGrade == BuildingGrade.Enum.Stone || targetGrade == BuildingGrade.Enum.Metal || targetGrade == BuildingGrade.Enum.TopTier; if (currentGrade == BuildingGrade.Enum.Wood) return targetGrade == BuildingGrade.Enum.Stone || targetGrade == BuildingGrade.Enum.Metal || targetGrade == BuildingGrade.Enum.TopTier; if (currentGrade == BuildingGrade.Enum.Stone) return targetGrade == BuildingGrade.Enum.Metal || targetGrade == BuildingGrade.Enum.TopTier; if (currentGrade == BuildingGrade.Enum.Metal) return targetGrade == BuildingGrade.Enum.TopTier; return false; } private List GetAvailableUpgradeTiers(List blocks) { var availableTiers = new HashSet(); var allTiers = new[] { BuildingGrade.Enum.Wood, BuildingGrade.Enum.Stone, BuildingGrade.Enum.Metal, BuildingGrade.Enum.TopTier }; foreach (var block in blocks) { foreach (var tier in allTiers) { if (CanUpgradeToTier(block.grade, tier)) { availableTiers.Add(tier); } else if (_config.EnableDowngrade && tier < block.grade && CanUpgradeToTier(block.grade, tier)) { availableTiers.Add(tier); } } } return availableTiers.ToList(); } private Dictionary CalculateTotalCost(List blocks, BuildingGrade.Enum targetGrade) { var totalCost = new Dictionary(); foreach (var block in blocks) { var cost = GetUpgradeCostFromGame(block, targetGrade); if (cost == null || cost.Count == 0) continue; foreach (var itemCost in cost) { int itemId = itemCost.itemDef.itemid; int amount = Mathf.CeilToInt(itemCost.amount); if (!totalCost.ContainsKey(itemId)) totalCost[itemId] = 0; totalCost[itemId] += amount; } } return totalCost; } private List GetUpgradeCostFromGame(BuildingBlock block, BuildingGrade.Enum targetGrade) { string cacheKey = $"{block.ShortPrefabName}_{block.grade}_{targetGrade}"; if (_costCache.TryGetValue(cacheKey, out var cached)) { return cached; } var costList = new List(); try { if (block.blockDefinition != null) { var constructionGrade = block.blockDefinition.GetGrade(targetGrade, 0); if (constructionGrade != null) { var costs = constructionGrade.CostToBuild(); if (costs != null && costs.Count > 0) { _costCache[cacheKey] = costs; return costs; } } } } catch { } _costCache[cacheKey] = costList; return costList; } private float GetMaxHPForTier(BuildingGrade.Enum tier) { switch(tier) { case BuildingGrade.Enum.Wood: return 250f; case BuildingGrade.Enum.Stone: return 500f; case BuildingGrade.Enum.Metal: return 1000f; case BuildingGrade.Enum.TopTier: return 2000f; default: return 250f; } } private List GetRepairCost(BuildingBlock block) { if (block == null || block.IsDestroyed) return new List(); float currentHP = block.health; float maxHP = GetMaxHPForTier(block.grade); float hpMissing = maxHP - currentHP; if (hpMissing <= 0.01f) return new List(); if (hpMissing < 1f) hpMissing = 1f; string itemShortname; float amountPerRepair; float hpPerRepair; switch(block.grade) { case BuildingGrade.Enum.Wood: itemShortname = "wood"; amountPerRepair = 40f; hpPerRepair = 51f; break; case BuildingGrade.Enum.Stone: itemShortname = "stones"; amountPerRepair = 30f; hpPerRepair = 50f; break; case BuildingGrade.Enum.Metal: itemShortname = "metal.fragments"; amountPerRepair = 10f; hpPerRepair = 50f; break; case BuildingGrade.Enum.TopTier: itemShortname = "metal.refined"; amountPerRepair = 1f; hpPerRepair = 79f; break; default: return new List(); } var itemDef = ItemManager.FindItemDefinition(itemShortname); if (itemDef == null) { return new List(); } int repairUnits = Mathf.CeilToInt(hpMissing / hpPerRepair); if (repairUnits < 1) repairUnits = 1; float totalAmount = repairUnits * amountPerRepair; return new List { new ItemAmount(itemDef, totalAmount) }; } private Dictionary GetAvailableResources(BuildingPrivlidge privilege) { var result = new Dictionary(); if (privilege?.inventory?.itemList == null) return result; foreach (var item in privilege.inventory.itemList) { if (!result.ContainsKey(item.info.itemid)) result[item.info.itemid] = 0; result[item.info.itemid] += item.amount; } return result; } private bool CanAffordUpgrade(Dictionary available, Dictionary totalCost, int blockCount) { if (blockCount == 0) return false; foreach (var cost in totalCost) { int needed = cost.Value; int have = available.TryGetValue(cost.Key, out var amt) ? amt : 0; if (have < needed) return false; } return true; } private int CalculateAffordableBlocks(Dictionary available, List blocks, BuildingGrade.Enum targetGrade) { int affordable = 0; var availableCopy = new Dictionary(available); foreach (var block in blocks) { var cost = GetUpgradeCostFromGame(block, targetGrade); bool canAfford = true; foreach (var itemCost in cost) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; int have = availableCopy.TryGetValue(itemId, out var amt) ? amt : 0; if (have < need) { canAfford = false; break; } } if (canAfford) { foreach (var itemCost in cost) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; availableCopy[itemId] -= need; } affordable++; } } return affordable; } private List GetDamagedBlocks(BuildingPrivlidge privilege, BasePlayer player) { var blocks = GetConnectedBuildingBlocks(privilege, player); var damagedBlocks = new List(); foreach (var block in blocks) { if (block == null || block.IsDestroyed) continue; float maxHP = GetMaxHPForTier(block.grade); if (block.health < maxHP) { damagedBlocks.Add(block); } } return damagedBlocks; } private Dictionary CalculateTotalRepairCost(List blocks) { var totalCost = new Dictionary(); if (blocks == null || blocks.Count == 0) return totalCost; foreach (var block in blocks) { if (block == null || block.IsDestroyed) continue; var cost = GetRepairCost(block); if (cost == null || cost.Count == 0) continue; foreach (var itemCost in cost) { if (itemCost?.itemDef == null) continue; int itemId = itemCost.itemDef.itemid; int amount = Mathf.CeilToInt(itemCost.amount); if (amount <= 0) continue; if (!totalCost.ContainsKey(itemId)) totalCost[itemId] = 0; totalCost[itemId] += amount; } } return totalCost; } private void PerformUpgrade(BasePlayer player, BuildingGrade.Enum targetGrade, bool fullUpgrade) { CloseUpgradeUI(player); if (IsUnderAttack(player)) { player.ChatMessage(Lang("Error.UnderAttack", player.UserIDString)); return; } var privilege = GetActiveToolCupboard(player); if (privilege == null) { return; } var blocks = GetConnectedBuildingBlocks(privilege, player); var eligibleBlocks = blocks.Where(b => b.grade < targetGrade && CanUpgradeToTier(b.grade, targetGrade)).ToList(); if (eligibleBlocks.Count == 0) { player.ChatMessage("No blocks can be upgraded to that tier."); return; } if (!fullUpgrade) { var available = GetAvailableResources(privilege); int affordable = CalculateAffordableBlocks(available, eligibleBlocks, targetGrade); eligibleBlocks = eligibleBlocks.Take(affordable).ToList(); } StartUpgradeQueue(player, privilege, eligibleBlocks, targetGrade); } private void StartUpgradeQueue(BasePlayer player, BuildingPrivlidge privilege, List blocks, BuildingGrade.Enum targetGrade) { if (blocks == null || blocks.Count == 0 || privilege == null || player == null) return; var currentPrivilege = GetActiveToolCupboard(player); if (currentPrivilege == null || currentPrivilege != privilege) { player.ChatMessage("You must be authorized on the Tool Cupboard to upgrade."); return; } var playerData = GetPlayerData(player.userID); string tierKey = targetGrade.ToString(); int lastUsedSkinId = 0; if (playerData.DefaultSkinPerTier.TryGetValue(tierKey, out var defaultSkinId)) { lastUsedSkinId = defaultSkinId; } else if (playerData.LastUsedSkin.TryGetValue(tierKey, out var savedSkinId)) { lastUsedSkinId = savedSkinId; } if (_activeQueues.TryGetValue(player.userID, out var existingQueue)) { existingQueue.Stop(); _activeQueues.Remove(player.userID); } var sortedBlocks = blocks.OrderBy(b => GetBlockUpgradePriority(b)).ToList(); bool upgradeEffectEnabled = _skinUpgradeEffectEnabled.TryGetValue(player.userID, out var enabled) && enabled; var queue = new UpgradeQueue { Player = player, Privilege = privilege, Blocks = sortedBlocks, TargetGrade = targetGrade, SkinId = (ulong)lastUsedSkinId, Index = 0, Upgraded = 0, Total = sortedBlocks.Count, UpgradeEffectEnabled = upgradeEffectEnabled }; _activeQueues[player.userID] = queue; string tierName = GetTierName(targetGrade); string speedText = _config.UpgradeIntervalSeconds == 0 ? $"Upgrading {sortedBlocks.Count} blocks to {tierName}..." : $"Upgrading {sortedBlocks.Count} blocks to {tierName} ({GetSpeedDisplay(_config.UpgradeIntervalSeconds)})..."; player.ChatMessage(speedText); ProcessUpgradeQueue(queue); } private ulong GetOldestFoundationId(BuildingBlock block) { if (block == null) return ulong.MaxValue; var building = block.GetBuilding(); if (building == null || building.buildingBlocks == null) return ulong.MaxValue; ulong oldestId = ulong.MaxValue; foreach (var buildingBlock in building.buildingBlocks) { if (buildingBlock == null) continue; if (!buildingBlock.ShortPrefabName.ToLower().Contains("foundation")) continue; ulong blockId = buildingBlock.net?.ID.Value ?? ulong.MaxValue; if (blockId < oldestId) oldestId = blockId; } return oldestId; } private bool IsBuildingBlocked(BuildingBlock block, BasePlayer player, BuildingPrivlidge activePrivilege = null) { if (block == null || player == null) return false; var blockPrivilege = block.GetBuildingPrivilege(); ulong blockBuildingId = GetOldestFoundationId(block); var nearbyBlocks = new List(); Vis.Entities(block.transform.position, 20f, nearbyBlocks); foreach (var nearbyBlock in nearbyBlocks) { if (nearbyBlock == null || nearbyBlock == block) continue; var nearbyBlockTC = nearbyBlock.GetBuildingPrivilege(); if (nearbyBlockTC == null) continue; if (nearbyBlockTC == activePrivilege) continue; if (!nearbyBlockTC.IsAuthed(player) || !nearbyBlockTC.CanBuild(player)) { ulong nearbyBuildingId = GetOldestFoundationId(nearbyBlock); if (nearbyBuildingId < blockBuildingId) return true; } } if (blockPrivilege != null) { if (!blockPrivilege.IsAuthed(player) || !blockPrivilege.CanBuild(player)) return true; } return false; } private int GetBlockUpgradePriority(BuildingBlock block) { if (block == null || string.IsNullOrEmpty(block.ShortPrefabName)) return 999; string prefabName = block.ShortPrefabName.ToLower(); if (prefabName.Contains("foundation")) return 1; if (prefabName.Contains("doorway") || prefabName.Contains("door.frame")) return 2; if (prefabName.Contains("wall") && !prefabName.Contains("doorway") && !prefabName.Contains("window")) return 3; if (prefabName.Contains("ceiling") || prefabName.Contains("floor") || prefabName.Contains("roof")) return 4; if (prefabName.Contains("window")) return 5; return 6; } private void ProcessUpgradeQueue(UpgradeQueue queue) { if (queue.IsStopped) return; if (queue.Player == null || queue.Privilege == null || queue.Index >= queue.Blocks.Count) { if (queue.Player != null && queue.Upgraded > 0) { queue.Player.ChatMessage($"Upgraded {queue.Upgraded} blocks to {GetTierName(queue.TargetGrade)}"); } _activeQueues.Remove(queue.Player?.userID ?? 0); DestroyUi(queue.Player); return; } if (IsUnderAttack(queue.Player)) { queue.Player.ChatMessage(Lang("Error.UpgradeInterrupted", queue.Player.UserIDString, queue.Upgraded, queue.Total)); _activeQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } if (queue.Privilege == null || queue.Privilege.IsDestroyed || !queue.Privilege.IsAuthed(queue.Player)) { queue.Index++; timer.Once(_config.UpgradeIntervalSeconds, () => ProcessUpgradeQueue(queue)); return; } var block = queue.Blocks[queue.Index]; int pieceNumber = queue.Index + 1; queue.Index++; if (!ValidateQueueBlock(block, queue.Privilege, queue.Player)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessUpgradeQueue(queue)); return; } if (IsBuildingBlocked(block, queue.Player, queue.Privilege)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessUpgradeQueue(queue)); return; } bool noCost = HasNoCostPermission(queue.Player); var cost = GetUpgradeCostFromGame(block, queue.TargetGrade); if (!CheckResources(queue.Privilege, cost, noCost)) { if (queue.Upgraded > 0) { queue.Player.ChatMessage(Lang("Error.UpgradePaused", queue.Player.UserIDString, queue.Upgraded, queue.Total)); } else { queue.Player.ChatMessage(Lang("Error.NoResources", queue.Player.UserIDString)); } _activeQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } ConsumeResources(queue.Privilege, cost, noCost); var originalGrade = block.grade; block.ChangeGradeAndSkin(queue.TargetGrade, queue.SkinId, true, true); if (block.grade == originalGrade && originalGrade != queue.TargetGrade) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessUpgradeQueue(queue)); return; } block.SetHealthToMax(); block.StartBeingRotatable(); queue.UpgradeEffectEnabled = _skinUpgradeEffectEnabled.TryGetValue(queue.Player.userID, out var effectEnabled) && effectEnabled; if (_config.UpgradeIntervalSeconds > 0) { if (queue.UpgradeEffectEnabled) { block.ClientRPC(null, "DoUpgradeEffect", (int)queue.TargetGrade, queue.SkinId); } } block.SendNetworkUpdate(); block.UpdateSkin(); if (queue.TargetGrade == BuildingGrade.Enum.Metal && queue.SkinId == 10221) { int colorIndex = -1; if (_selectedColorIndex.TryGetValue(queue.Player.userID, out var selectedIdx)) { colorIndex = selectedIdx; } else { var playerData = GetPlayerData(queue.Player.userID); if (playerData.SelectedColorIndex >= 0) { colorIndex = playerData.SelectedColorIndex; _selectedColorIndex[queue.Player.userID] = colorIndex; } } if (colorIndex >= 0 && colorIndex < ColorPalette.Length) { int actualColor = colorIndex == 0 ? GetRandomColor() : colorIndex; block.UpdateSkin(); block.SetCustomColour((uint)actualColor); block.SendNetworkUpdateImmediate(); } } block.ResetUpkeepTime(); block.UpdateSurroundingEntities(); BuildingManager.server.GetBuilding(block.buildingID)?.Dirty(); if (_config.UpgradeIntervalSeconds > 0 && _config.EnableUpgradeAnimation && queue.TargetGrade > BuildingGrade.Enum.Twigs) { string gradeName = queue.TargetGrade.ToString().ToLower(); Effect.server.Run($"assets/bundled/prefabs/fx/build/promote_{gradeName}.prefab", block, 0u, Vector3.zero, Vector3.zero); } queue.Upgraded++; timer.Once(_config.UpgradeIntervalSeconds, () => ProcessUpgradeQueue(queue)); } private void StartRepairQueue(BasePlayer player, BuildingPrivlidge privilege, List blocks) { if (blocks == null || blocks.Count == 0 || privilege == null || player == null) return; var currentPrivilege = GetActiveToolCupboard(player); if (currentPrivilege == null || currentPrivilege != privilege) { player.ChatMessage("You must be authorized on the Tool Cupboard to repair."); return; } if (_activeRepairQueues.TryGetValue(player.userID, out var existingQueue)) { existingQueue.Stop(); _activeRepairQueues.Remove(player.userID); } var sortedBlocks = blocks.OrderBy(b => GetBlockUpgradePriority(b)).ToList(); var queue = new RepairQueue { Player = player, Privilege = privilege, Blocks = sortedBlocks, Index = 0, Repaired = 0, Total = sortedBlocks.Count }; _activeRepairQueues[player.userID] = queue; string speedText = _config.UpgradeIntervalSeconds == 0 ? $"Repairing {sortedBlocks.Count} blocks..." : $"Repairing {sortedBlocks.Count} blocks ({GetSpeedDisplay(_config.UpgradeIntervalSeconds)})..."; player.ChatMessage(speedText); ProcessRepairQueue(queue); } private void ProcessRepairQueue(RepairQueue queue) { if (queue.IsStopped) return; if (queue.Player == null || queue.Privilege == null || queue.Index >= queue.Blocks.Count) { if (queue.Player != null && queue.Repaired > 0) { queue.Player.ChatMessage($"Repaired {queue.Repaired} blocks"); } _activeRepairQueues.Remove(queue.Player?.userID ?? 0); DestroyUi(queue.Player); return; } if (IsUnderAttack(queue.Player)) { queue.Player.ChatMessage(Lang("Error.RepairInterrupted", queue.Player.UserIDString, queue.Repaired, queue.Total)); _activeRepairQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } if (queue.Privilege == null || queue.Privilege.IsDestroyed || !queue.Privilege.IsAuthed(queue.Player) || !queue.Privilege.CanBuild(queue.Player)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } var block = queue.Blocks[queue.Index]; int pieceNumber = queue.Index + 1; queue.Index++; if (block == null || block.IsDestroyed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } var blockPrivilege = block.GetBuildingPrivilege(); bool blockMatchesTC = blockPrivilege == queue.Privilege; bool tcCanBuild = queue.Privilege.CanBuild(queue.Player); bool isAuthed = queue.Privilege.IsAuthed(queue.Player); if (!blockMatchesTC) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } if (!tcCanBuild || !isAuthed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } if (IsBuildingBlocked(block, queue.Player, queue.Privilege)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } float maxHP = GetMaxHPForTier(block.grade); if (block.health >= maxHP) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } bool noCost = HasNoCostPermission(queue.Player); var cost = GetRepairCost(block); if (cost == null || cost.Count == 0) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } var available = GetAvailableResources(queue.Privilege); bool canAfford = true; if (!noCost) { foreach (var itemCost in cost) { if (itemCost?.itemDef == null) continue; int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; int have = available.TryGetValue(itemId, out var amt) ? amt : 0; if (have < need) { canAfford = false; break; } } } if (!canAfford && !noCost) { bool hasMoreBlocks = false; for (int i = queue.Index; i < queue.Blocks.Count; i++) { var nextBlock = queue.Blocks[i]; if (nextBlock == null || nextBlock.IsDestroyed) continue; float nextMaxHP = GetMaxHPForTier(nextBlock.grade); if (nextBlock.health >= nextMaxHP) continue; var nextCost = GetRepairCost(nextBlock); if (nextCost == null || nextCost.Count == 0) continue; bool canAffordNext = noCost; if (!noCost) { canAffordNext = true; foreach (var itemCost in nextCost) { if (itemCost?.itemDef == null) continue; int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; int have = available.TryGetValue(itemId, out var amt) ? amt : 0; if (have < need) { canAffordNext = false; break; } } } if (canAffordNext) { hasMoreBlocks = true; break; } } if (!hasMoreBlocks) { if (queue.Repaired > 0) { queue.Player.ChatMessage($"Repair completed: Repaired {queue.Repaired}/{queue.Total} blocks. Not enough resources for remaining blocks."); } else { queue.Player.ChatMessage("Repair failed: Not enough resources to repair any blocks."); } _activeRepairQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); return; } if (!noCost) { foreach (var itemCost in cost) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; var items = queue.Privilege.inventory.itemList.Where(i => i.info.itemid == itemId).ToList(); foreach (var item in items) { if (need <= 0) break; int remove = Math.Min(item.amount, need); item.UseItem(remove); need -= remove; } } } Effect.server.Run("assets/bundled/prefabs/fx/build/repair.prefab", block.transform.position); block.SetHealthToMax(); block.SendNetworkUpdate(); queue.Repaired++; timer.Once(_config.UpgradeIntervalSeconds, () => ProcessRepairQueue(queue)); } private void StartDowngradeQueue(BasePlayer player, BuildingPrivlidge privilege, List blocks, BuildingGrade.Enum targetGrade) { if (blocks == null || blocks.Count == 0 || privilege == null || player == null) return; if (!_config.EnableDowngrade) { player.ChatMessage(Lang("Error.DowngradeDisabled", player.UserIDString)); return; } if (!HasDowngradePermission(player)) { player.ChatMessage("You don't have permission to downgrade."); return; } var currentPrivilege = GetActiveToolCupboard(player); if (currentPrivilege == null || currentPrivilege != privilege) { player.ChatMessage("You must be authorized on the Tool Cupboard to downgrade."); return; } if (_activeDowngradeQueues.TryGetValue(player.userID, out var existingQueue)) { existingQueue.Stop(); _activeDowngradeQueues.Remove(player.userID); } var sortedBlocks = blocks.OrderBy(b => GetBlockUpgradePriority(b)).ToList(); var playerData = GetPlayerData(player.userID); string tierKey = targetGrade.ToString(); int skinId = GetDefaultSkinContentId(player, targetGrade); if (skinId == 0 && playerData.LastUsedSkin.TryGetValue(tierKey, out var savedSkinId)) skinId = savedSkinId; var queue = new DowngradeQueue { Player = player, Privilege = privilege, Blocks = sortedBlocks, TargetGrade = targetGrade, SkinId = (ulong)skinId, Index = 0, Downgraded = 0, Total = sortedBlocks.Count }; _activeDowngradeQueues[player.userID] = queue; string tierName = GetTierName(targetGrade); string speedText = _config.UpgradeIntervalSeconds == 0 ? $"Downgrading {sortedBlocks.Count} blocks to {tierName}..." : $"Downgrading {sortedBlocks.Count} blocks to {tierName} ({GetSpeedDisplay(_config.UpgradeIntervalSeconds)})..."; player.ChatMessage(speedText); ProcessDowngradeQueue(queue); } private void ProcessDowngradeQueue(DowngradeQueue queue) { if (queue.IsStopped) return; if (queue.Player == null || queue.Privilege == null || queue.Index >= queue.Blocks.Count) { if (queue.Player != null && queue.Downgraded > 0) { queue.Player.ChatMessage($"Downgraded {queue.Downgraded} blocks to {GetTierName(queue.TargetGrade)}"); } _activeDowngradeQueues.Remove(queue.Player?.userID ?? 0); DestroyUi(queue.Player); return; } if (IsUnderAttack(queue.Player)) { queue.Player.ChatMessage(Lang("Error.DowngradeInterrupted", queue.Player.UserIDString, queue.Downgraded, queue.Total)); _activeDowngradeQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } if (queue.Privilege == null || queue.Privilege.IsDestroyed || !queue.Privilege.IsAuthed(queue.Player) || !queue.Privilege.CanBuild(queue.Player)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } var block = queue.Blocks[queue.Index]; int pieceNumber = queue.Index + 1; queue.Index++; if (block == null || block.IsDestroyed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } var blockPrivilege = block.GetBuildingPrivilege(); bool blockMatchesTC = blockPrivilege == queue.Privilege; bool tcCanBuild = queue.Privilege.CanBuild(queue.Player); bool isAuthed = queue.Privilege.IsAuthed(queue.Player); if (!blockMatchesTC) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } if (!tcCanBuild || !isAuthed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } if (IsBuildingBlocked(block, queue.Player, queue.Privilege)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } if (block.grade <= queue.TargetGrade) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } bool noCost = HasNoCostPermission(queue.Player); var cost = GetUpgradeCostFromGame(block, queue.TargetGrade); if (!noCost) { var available = GetAvailableResources(queue.Privilege); bool canAfford = true; foreach (var itemCost in cost) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; int have = available.TryGetValue(itemId, out var amt) ? amt : 0; if (have < need) { canAfford = false; break; } } if (!canAfford) { if (queue.Downgraded > 0) { queue.Player.ChatMessage(Lang("Error.DowngradePaused", queue.Player.UserIDString, queue.Downgraded, queue.Total)); } _activeDowngradeQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } foreach (var itemCost in cost) { int need = Mathf.CeilToInt(itemCost.amount); int itemId = itemCost.itemDef.itemid; var items = queue.Privilege.inventory.itemList.Where(i => i.info.itemid == itemId).ToList(); foreach (var item in items) { if (need <= 0) break; int remove = Math.Min(item.amount, need); item.UseItem(remove); need -= remove; } } } var originalGrade = block.grade; block.ChangeGradeAndSkin(queue.TargetGrade, queue.SkinId, true, true); if (block.grade == originalGrade && originalGrade != queue.TargetGrade) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); return; } block.SetHealthToMax(); block.StartBeingRotatable(); bool upgradeEffectEnabled = _skinUpgradeEffectEnabled.TryGetValue(queue.Player.userID, out var effectEnabled) && effectEnabled; if (_config.UpgradeIntervalSeconds > 0) { if (upgradeEffectEnabled) { block.ClientRPC(null, "DoUpgradeEffect", (int)queue.TargetGrade, queue.SkinId); } } block.SendNetworkUpdate(); block.UpdateSkin(); block.ResetUpkeepTime(); block.UpdateSurroundingEntities(); BuildingManager.server.GetBuilding(block.buildingID)?.Dirty(); if (_config.UpgradeIntervalSeconds > 0 && _config.EnableUpgradeAnimation && queue.TargetGrade > BuildingGrade.Enum.Twigs) { string gradeName = queue.TargetGrade.ToString().ToLower(); Effect.server.Run($"assets/bundled/prefabs/fx/build/promote_{gradeName}.prefab", block, 0u, Vector3.zero, Vector3.zero); } queue.Downgraded++; timer.Once(_config.UpgradeIntervalSeconds, () => ProcessDowngradeQueue(queue)); } #region Wallpaper Helper Methods private List FilterBlocksForWallpaper(List blocks, ulong wallpaperId, string blockType) { var result = new List(); string blockTypeLower = blockType.ToLower(); var processedIds = new HashSet(); bool isFoundation = blockTypeLower == "foundation"; foreach (var block in blocks) { if (block == null || block.IsDestroyed) continue; ulong blockId = block.net?.ID.Value ?? 0; if (processedIds.Contains(blockId)) continue; processedIds.Add(blockId); bool shouldInclude = false; var (isFoundationBlock, _, isRoofOrCeiling, isFloorBlock) = GetBlockTypeInfo(block.ShortPrefabName); bool isFloorOrRoofBlock = isFloorBlock || isRoofOrCeiling; if (blockTypeLower == "wall") { shouldInclude = ShouldIncludeWallForWallpaper(block, wallpaperId); } else if (blockTypeLower == "foundation") { if (isFoundationBlock) { shouldInclude = NeedsWallpaperUpdate(block, wallpaperId, 0); } else if (isFloorBlock) { shouldInclude = IsFloorSide1Internal(block) && NeedsWallpaperUpdate(block, wallpaperId, 1); } } else if (blockTypeLower == "ceiling") { if (isFloorOrRoofBlock) { shouldInclude = NeedsWallpaperUpdate(block, wallpaperId, 0); } } else { if (isFoundationBlock) { shouldInclude = NeedsWallpaperUpdate(block, wallpaperId, 0); } } if (shouldInclude) result.Add(block); } return result; } private ulong GetWallpaperId(BuildingBlock block, int side) => side == 0 ? block.wallpaperID : block.wallpaperID2; private bool NeedsWallpaperUpdate(BuildingBlock block, ulong wallpaperId, int side) { bool hasWallpaper = block.HasWallpaper(side); ulong currentId = hasWallpaper ? GetWallpaperId(block, side) : 0; bool needsUpdate = false; if (!hasWallpaper) { needsUpdate = wallpaperId != 1; } else if (wallpaperId == 1) needsUpdate = true; else if (wallpaperId == 0) needsUpdate = currentId != 0; else needsUpdate = currentId != wallpaperId; return needsUpdate; } private bool ShouldIncludeWallForWallpaper(BuildingBlock block, ulong wallpaperId) { if (NeedsWallpaperUpdate(block, wallpaperId, 0)) return true; return IsWallSideInternal(block, 1) && NeedsWallpaperUpdate(block, wallpaperId, 1); } private bool IsWallType(string blockType) => blockType.ToLower() == "wall"; private int RemoveWallpaperFromBlock(BuildingBlock block, string blockType) { int removed = 0; bool isWall = IsWallType(blockType); bool isFoundation = blockType.ToLower() == "foundation"; bool isCeiling = blockType.ToLower() == "ceiling"; var (isFoundationBlock, _, isRoofOrCeiling, isFloorBlock) = GetBlockTypeInfo(block.ShortPrefabName); bool isFloorOrRoofBlock = isFloorBlock || isRoofOrCeiling; if (isFoundation && isFoundationBlock && !isFloorBlock && block.HasWallpaper(0)) { block.RemoveWallpaper(0); removed++; } if (isWall) { if (block.HasWallpaper(0)) { block.RemoveWallpaper(0); removed++; } if (block.HasWallpaper(1)) { block.RemoveWallpaper(1); removed++; } } if (isCeiling && isFloorOrRoofBlock && block.HasWallpaper(0)) { block.RemoveWallpaper(0); removed++; } if (isFoundation) { if (isFloorOrRoofBlock && IsFloorSide1Internal(block) && block.HasWallpaper(1)) { block.RemoveWallpaper(1); removed++; } } return removed; } private int ApplyWallpaperToBlock(BuildingBlock block, ulong wallpaperId, string blockType, BasePlayer player = null) { int blocksProcessed = 0; bool isWall = IsWallType(blockType); bool isFoundation = blockType.Equals("foundation", StringComparison.OrdinalIgnoreCase); bool isCeiling = blockType.Equals("ceiling", StringComparison.OrdinalIgnoreCase); var (isFoundationBlock, _, isRoofOrCeiling, isFloorBlock) = GetBlockTypeInfo(block.ShortPrefabName); bool isFloorOrRoofBlock = isFloorBlock || isRoofOrCeiling; if (isCeiling && isFloorOrRoofBlock && NeedsWallpaperUpdate(block, wallpaperId, 0)) { block.SetWallpaper(wallpaperId, 0, 0f); blocksProcessed++; } if (isFoundation && isFoundationBlock && !isFloorBlock && NeedsWallpaperUpdate(block, wallpaperId, 0)) { block.SetWallpaper(wallpaperId, 0, 0f); blocksProcessed++; } if (isWall) { if (NeedsWallpaperUpdate(block, wallpaperId, 0)) { block.SetWallpaper(wallpaperId, 0, 0f); blocksProcessed++; } if (IsWallSideInternal(block, 1) && NeedsWallpaperUpdate(block, wallpaperId, 1)) { block.SetWallpaper(wallpaperId, 1, 0f); blocksProcessed++; } } if (isFoundation) { if (isFloorOrRoofBlock && IsFloorSide1Internal(block) && NeedsWallpaperUpdate(block, wallpaperId, 1)) { block.SetWallpaper(wallpaperId, 1, 0f); blocksProcessed++; } } return blocksProcessed; } private bool HandleWallpaperResources(WallpaperQueue queue, int piecesRemoved) { bool noCost = HasNoCostPermission(queue.Player); if (queue.WallpaperId == 1) { if (!noCost && piecesRemoved > 0) { int clothToRefund = piecesRemoved * 5; GiveResourceAmount(queue.Privilege, ClothItemId, clothToRefund); } return true; } else { if (!noCost) { int clothNeeded = 5; if (!CheckResourceAmount(queue.Privilege, ClothItemId, clothNeeded, noCost)) { var available = GetAvailableResources(queue.Privilege); int have = available.TryGetValue(ClothItemId, out var amt) ? amt : 0; if (queue.Applied > 0) { queue.Player.ChatMessage(Lang("Error.WallpaperPaused", queue.Player.UserIDString, clothNeeded, have, queue.Applied, queue.Total)); } else { queue.Player.ChatMessage(Lang("Error.NoResources", queue.Player.UserIDString)); } _activeWallpaperQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return false; } ConsumeResourceAmount(queue.Privilege, ClothItemId, clothNeeded, noCost); } return true; } } #endregion private static string GetWallpaperBlockTypeDisplayName(string blockType) { if (string.IsNullOrEmpty(blockType)) return blockType; string lower = blockType.ToLowerInvariant(); if (lower == "foundation") return "floor"; if (lower == "wall") return "wall"; if (lower == "ceiling") return "ceiling"; return blockType; } private void StartWallpaperQueue(BasePlayer player, BuildingPrivlidge privilege, List blocks, int contentId, string blockType, ulong wallpaperId) { if (blocks == null || blocks.Count == 0 || privilege == null || player == null) return; var currentPrivilege = GetActiveToolCupboard(player); if (currentPrivilege == null || currentPrivilege != privilege) { player.ChatMessage("You must be authorized on the Tool Cupboard to apply wallpaper."); return; } if (_activeWallpaperQueues.TryGetValue(player.userID, out var existingQueue)) { existingQueue.Stop(); _activeWallpaperQueues.Remove(player.userID); } var filteredBlocks = FilterBlocksForWallpaper(blocks, wallpaperId, blockType); if (filteredBlocks.Count == 0) { string displayName = GetWallpaperBlockTypeDisplayName(blockType); player.ChatMessage($"All {displayName} blocks already have this wallpaper applied."); return; } var sortedBlocks = filteredBlocks.OrderBy(b => GetBlockUpgradePriority(b)).ToList(); string wallpaperName = contentId == 1 ? "None" : (contentId == 0 ? "Default" : $"Wallpaper {contentId}"); var queue = new WallpaperQueue { Player = player, Privilege = privilege, Blocks = sortedBlocks, ContentId = contentId, BlockType = blockType, WallpaperId = wallpaperId, Index = 0, Applied = 0, Total = sortedBlocks.Count }; _activeWallpaperQueues[player.userID] = queue; player.ChatMessage($"Applying wallpaper to {sortedBlocks.Count} blocks..."); ProcessWallpaperQueue(queue); } private void ProcessWallpaperQueue(WallpaperQueue queue) { if (queue.IsStopped) return; if (queue.Player == null || queue.Privilege == null || queue.Index >= queue.Blocks.Count) { if (queue.Player != null && queue.Applied > 0) { queue.Player.ChatMessage("Wallpaper applied."); } _activeWallpaperQueues.Remove(queue.Player?.userID ?? 0); DestroyUi(queue.Player); return; } if (IsUnderAttack(queue.Player)) { queue.Player.ChatMessage(Lang("Wallpaper.CompletedCount", queue.Player.UserIDString, queue.Applied, queue.Total)); _activeWallpaperQueues.Remove(queue.Player.userID); DestroyUi(queue.Player); return; } if (queue.Privilege == null || queue.Privilege.IsDestroyed || !queue.Privilege.IsAuthed(queue.Player)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessWallpaperQueue(queue)); return; } var block = queue.Blocks[queue.Index]; int pieceNumber = queue.Index + 1; queue.Index++; if (block == null || block.IsDestroyed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessWallpaperQueue(queue)); return; } var blockPrivilege = block.GetBuildingPrivilege(); bool blockMatchesTC = blockPrivilege == queue.Privilege; if (!blockMatchesTC) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessWallpaperQueue(queue)); return; } if (IsBuildingBlocked(block, queue.Player, queue.Privilege)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessWallpaperQueue(queue)); return; } bool wallpaperApplied = false; int piecesProcessed = 0; try { if (queue.WallpaperId == 1) { piecesProcessed = RemoveWallpaperFromBlock(block, queue.BlockType); wallpaperApplied = piecesProcessed > 0; if (wallpaperApplied && HandleWallpaperResources(queue, piecesProcessed)) { block.SendNetworkUpdate(); Effect.server.Run("assets/prefabs/wallpaper/effects/place.prefab", block.transform.position); queue.Applied++; } else if (wallpaperApplied) { return; } } else { if (!HandleWallpaperResources(queue, 0)) return; piecesProcessed = ApplyWallpaperToBlock(block, queue.WallpaperId, queue.BlockType, queue.Player); wallpaperApplied = piecesProcessed > 0; if (wallpaperApplied) { block.SendNetworkUpdate(); Effect.server.Run("assets/prefabs/wallpaper/effects/place.prefab", block.transform.position); queue.Applied++; } } } catch { } timer.Once(_config.UpgradeIntervalSeconds, () => ProcessWallpaperQueue(queue)); } private void StartSkinQueue(BasePlayer player, BuildingPrivlidge privilege, List blocks, int contentId, BuildingGrade.Enum? targetTier) { if (blocks == null || blocks.Count == 0 || privilege == null || player == null) return; var currentPrivilege = GetActiveToolCupboard(player); if (currentPrivilege == null || currentPrivilege != privilege) { player.ChatMessage("You must be authorized on the Tool Cupboard to apply skins."); return; } if (_activeSkinQueues.TryGetValue(player.userID, out var existingQueue)) { existingQueue.Stop(); _activeSkinQueues.Remove(player.userID); } var queue = new SkinQueue { Player = player, Privilege = privilege, Blocks = new List(blocks), ContentId = contentId, TargetTier = targetTier, Index = 0, Applied = 0, Total = blocks.Count }; _activeSkinQueues[player.userID] = queue; string skinName = GetSkinName(contentId, targetTier); string tierName = targetTier.HasValue ? GetTierName(targetTier.Value) : "all tiers"; string speedText = _config.UpgradeIntervalSeconds == 0 ? $"Applying {skinName} to {blocks.Count} blocks..." : $"Applying {skinName} to {blocks.Count} blocks ({GetSpeedDisplay(_config.UpgradeIntervalSeconds)})..."; player.ChatMessage(speedText); ProcessSkinQueue(queue); } private void ProcessSkinQueue(SkinQueue queue) { if (queue.IsStopped) return; if (queue.Player == null || queue.Privilege == null || queue.Index >= queue.Blocks.Count) { if (queue.Player != null && queue.Applied > 0) { string skinName = GetSkinName(queue.ContentId, queue.TargetTier); string tierName = queue.TargetTier.HasValue ? GetTierName(queue.TargetTier.Value) : "all tiers"; queue.Player.ChatMessage($"Applied {skinName} to {queue.Applied} {tierName} blocks"); CuiHelper.DestroyUi(queue.Player, "SkinPanel"); CuiHelper.DestroyUi(queue.Player, "SkinContentPanel"); CuiHelper.DestroyUi(queue.Player, "SettingsButton"); CuiHelper.DestroyUi(queue.Player, "SettingsPanel"); _settingsPanelOpen.Remove(queue.Player.userID); _openPanels.Remove(queue.Player.userID); } _activeSkinQueues.Remove(queue.Player?.userID ?? 0); return; } if (IsUnderAttack(queue.Player)) { queue.Player.ChatMessage(Lang("Error.SkinInterrupted", queue.Player.UserIDString, queue.Applied, queue.Total)); _activeSkinQueues.Remove(queue.Player.userID); return; } if (queue.Privilege == null || queue.Privilege.IsDestroyed || !queue.Privilege.IsAuthed(queue.Player) || !queue.Privilege.CanBuild(queue.Player)) { queue.Stop(); _activeSkinQueues.Remove(queue.Player?.userID ?? 0); if (queue.Player != null && queue.Applied > 0) { string skinName = GetSkinName(queue.ContentId, queue.TargetTier); string tierName = queue.TargetTier.HasValue ? GetTierName(queue.TargetTier.Value) : "all tiers"; queue.Player.ChatMessage($"Skin application stopped. Applied {skinName} to {queue.Applied} {tierName} blocks."); } return; } var block = queue.Blocks[queue.Index]; queue.Index++; if (block == null || block.IsDestroyed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessSkinQueue(queue)); return; } if (IsBuildingBlocked(block, queue.Player, queue.Privilege)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessSkinQueue(queue)); return; } if (block.blockDefinition?.grades != null) { bool isValid = false; foreach (var grade in block.blockDefinition.grades) { if (grade?.gradeBase != null && grade.gradeBase.type == block.grade && grade.gradeBase.skin == (ulong)queue.ContentId) { isValid = true; break; } } if (isValid) { ulong skinID = (ulong)queue.ContentId; block.skinID = skinID; block.UpdateSkin(); if (skinID == 10221) { var playerData = GetPlayerData(queue.Player.userID); int? colorIndex = null; if (_selectedColorIndex.TryGetValue(queue.Player.userID, out var selectedIdx) && selectedIdx >= 0 && selectedIdx < ColorPalette.Length) { colorIndex = selectedIdx; } else if (playerData.SelectedColorIndex >= 0 && playerData.SelectedColorIndex < ColorPalette.Length) { colorIndex = playerData.SelectedColorIndex; } if (colorIndex.HasValue) { int actualColor = colorIndex.Value == 0 ? GetRandomColor() : colorIndex.Value; block.SetCustomColour((uint)actualColor); } } block.SendNetworkUpdateImmediate(); if (_config.UpgradeIntervalSeconds > 0) { if (_skinUpgradeEffectEnabled.TryGetValue(queue.Player.userID, out var effectEnabled) && effectEnabled) { block.ClientRPC(null, "DoUpgradeEffect", (int)block.grade, skinID); } if (_config.EnableUpgradeAnimation && block.grade > BuildingGrade.Enum.Twigs) { string gradeName = block.grade.ToString().ToLower(); Effect.server.Run($"assets/bundled/prefabs/fx/build/promote_{gradeName}.prefab", block, 0u, Vector3.zero, Vector3.zero); } } block.UpdateSurroundingEntities(); BuildingManager.server.GetBuilding(block.buildingID)?.Dirty(); queue.Applied++; } else { } } else { } timer.Once(_config.UpgradeIntervalSeconds, () => ProcessSkinQueue(queue)); } private void StartColorQueue(BasePlayer player, BuildingPrivlidge privilege, List blocks, int colorIndex) { if (blocks == null || blocks.Count == 0 || privilege == null || player == null) return; var currentPrivilege = GetActiveToolCupboard(player); if (currentPrivilege == null || currentPrivilege != privilege) { player.ChatMessage("You must be authorized on the Tool Cupboard to change colors."); return; } if (_activeColorQueues.TryGetValue(player.userID, out var existingQueue)) { existingQueue.Stop(); _activeColorQueues.Remove(player.userID); } var queue = new ColorQueue { Player = player, Privilege = privilege, Blocks = new List(blocks), ColorIndex = colorIndex, Index = 0, Applied = 0, Total = blocks.Count }; _activeColorQueues[player.userID] = queue; string speedText = _config.UpgradeIntervalSeconds == 0 ? $"Changing color on {blocks.Count} container pieces..." : $"Changing color on {blocks.Count} container pieces ({GetSpeedDisplay(_config.UpgradeIntervalSeconds)})..."; player.ChatMessage(speedText); ProcessColorQueue(queue); } private void ProcessColorQueue(ColorQueue queue) { if (queue.IsStopped) return; if (queue.Player == null || queue.Privilege == null || queue.Index >= queue.Blocks.Count) { if (queue.Player != null && queue.Applied > 0) { queue.Player.ChatMessage($"Changed color on {queue.Applied} container pieces"); } _activeColorQueues.Remove(queue.Player?.userID ?? 0); return; } if (IsUnderAttack(queue.Player)) { queue.Player.ChatMessage(Lang("Error.ColorInterrupted", queue.Player.UserIDString, queue.Applied, queue.Total)); _activeColorQueues.Remove(queue.Player.userID); return; } if (queue.Privilege == null || queue.Privilege.IsDestroyed || !queue.Privilege.IsAuthed(queue.Player) || !queue.Privilege.CanBuild(queue.Player)) { queue.Stop(); _activeColorQueues.Remove(queue.Player?.userID ?? 0); if (queue.Player != null && queue.Applied > 0) { queue.Player.ChatMessage($"Color change stopped. Changed color on {queue.Applied} container pieces."); } return; } var block = queue.Blocks[queue.Index]; queue.Index++; if (block == null || block.IsDestroyed) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessColorQueue(queue)); return; } if (IsBuildingBlocked(block, queue.Player, queue.Privilege)) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessColorQueue(queue)); return; } int currentColorIndex = GetBlockColorIndex(block); if (queue.ColorIndex != 0 && currentColorIndex == queue.ColorIndex) { timer.Once(_config.UpgradeIntervalSeconds, () => ProcessColorQueue(queue)); return; } { int currentColorBefore = GetBlockColorIndex(block); int targetColor = queue.ColorIndex == 0 ? GetRandomColor() : queue.ColorIndex; block.UpdateSkin(); block.SetCustomColour((uint)targetColor); block.SendNetworkUpdateImmediate(); if (_config.UpgradeIntervalSeconds > 0) { const string fxspray = "assets/prefabs/deployable/repair bench/effects/skinchange_spraypaint.prefab"; const string fxreskin = "assets/prefabs/tools/spraycan/reskineffect.prefab"; Effect.server.Run(fxspray, block.transform.position); Effect.server.Run(fxreskin, block.transform.position); } queue.Applied++; } timer.Once(_config.UpgradeIntervalSeconds, () => ProcessColorQueue(queue)); } private class UpgradeQueue { public BasePlayer Player; public BuildingPrivlidge Privilege; public List Blocks; public BuildingGrade.Enum TargetGrade; public ulong SkinId; public int Index; public int Upgraded; public int Total; public bool UpgradeEffectEnabled; public bool IsStopped; public void Stop() { IsStopped = true; } } private class SkinQueue { public BasePlayer Player; public BuildingPrivlidge Privilege; public List Blocks; public int ContentId; public BuildingGrade.Enum? TargetTier; public int Index; public int Applied; public int Total; public bool IsStopped; public void Stop() { IsStopped = true; } } private class ColorQueue { public BasePlayer Player; public BuildingPrivlidge Privilege; public List Blocks; public int ColorIndex; public int Index; public int Applied; public int Total; public bool IsStopped; public void Stop() { IsStopped = true; } } private class RepairQueue { public BasePlayer Player; public BuildingPrivlidge Privilege; public List Blocks; public int Index; public int Repaired; public int Total; public bool IsStopped; public void Stop() { IsStopped = true; } } private class DowngradeQueue { public BasePlayer Player; public BuildingPrivlidge Privilege; public List Blocks; public BuildingGrade.Enum TargetGrade; public ulong SkinId; public int Index; public int Downgraded; public int Total; public bool IsStopped; public void Stop() { IsStopped = true; } } private class WallpaperQueue { public BasePlayer Player; public BuildingPrivlidge Privilege; public List Blocks; public int ContentId; public string BlockType; public ulong WallpaperId; public int Index; public int Applied; public int Total; public bool IsStopped; public void Stop() { IsStopped = true; } } #endregion #region Harmony Patches private void InitializeHarmonyPatches() { _harmony = new Harmony("com.uzumi.cupboardplus"); RegisterHarmonyPatch(typeof(WallpaperRemovalHandler)); } private void RegisterHarmonyPatch(Type patchType) { try { var harmonyAttributes = patchType.GetCustomAttributes(typeof(HarmonyPatch), false); if (harmonyAttributes.Length > 0 && _harmony != null) { _harmony.CreateClassProcessor(patchType).Patch(); } } catch { } } [HarmonyPatch(typeof(BuildingBlock), "RPC_PickupWallpaperStart")] public static class WallpaperRemovalHandler { [HarmonyPrefix] public static bool Prefix(BuildingBlock __instance, BaseEntity.RPCMessage msg) { if (PluginInstance == null || msg.player == null) return true; try { if (!msg.player.CanInteract() || !__instance.ShouldDisplayPickupOption(msg.player) || !__instance.CanCompletePickup(msg.player)) return false; bool isSideZero = msg.read.Bool(); int sideIndex = isSideZero ? 0 : 1; if (!__instance.HasWallpaper(sideIndex)) return false; bool hasNoCostPermission = PluginInstance.permission.UserHasPermission(msg.player.UserIDString, "cupboardplus.nocost"); if (!hasNoCostPermission) { Item clothItem = ItemManager.CreateByItemID(-858312878, 5); if (clothItem != null) { msg.player.GiveItem(clothItem, BaseEntity.GiveItemReason.PickedUp); } } __instance.RemoveWallpaper(sideIndex); return false; } catch { return true; } } } #endregion #region Wallpaper Hooks private object OnPayForPlacement(BasePlayer player, WallpaperPlanner instance, Construction component) { try { // Only handle WallpaperPlanner placement; bail if called for Planner (building block) or invalid args if (player == null || instance == null || component == null) return null; // Avoid accessing component.gameObject on destroyed Unity objects (can throw MissingReferenceException) if (!component || !component.gameObject) return null; // Construction from WallpaperPlanner may not have a BuildingBlock (e.g. preview); null check to avoid crash if (!component.TryGetComponent(out var targetBlock) || targetBlock == null) return null; _pendingWallpaperFromTool[player.userID] = (targetBlock, 0, 0UL); return null; } catch { return null; } } private object OnWallpaperSet(BuildingBlock buildingBlock, ulong id, int side, float rotation) { if (buildingBlock == null) return null; BasePlayer triggeringPlayer = null; bool isFromTool = false; foreach (var kvp in _pendingWallpaperFromTool.ToList()) { var (block, _, _) = kvp.Value; if (block == buildingBlock) { triggeringPlayer = BasePlayer.FindByID(kvp.Key); isFromTool = true; _pendingWallpaperFromTool.Remove(kvp.Key); break; } } if (!isFromTool) { foreach (var player in BasePlayer.activePlayerList) { if (player == null) continue; var activeItem = player.GetActiveItem(); if (activeItem != null) { var heldEntity = activeItem.GetHeldEntity(); if (heldEntity != null && heldEntity.GetType().Name == "WallpaperPlanner") { var hit = GetLookHit(player); if (hit.HasValue) { var hitValue = hit.Value; var hitEntity = hitValue.GetEntity(); if (hitEntity == buildingBlock) { triggeringPlayer = player; isFromTool = true; break; } } } } } } if (isFromTool && triggeringPlayer != null) { } return null; } private RaycastHit? GetLookHit(BasePlayer player) { if (player == null) return null; RaycastHit hit; if (Physics.Raycast(player.eyes.HeadRay(), out hit, 3f)) { return hit; } return null; } #endregion } }