using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using Oxide.Core; using Oxide.Core.Configuration; using Oxide.Core.Plugins; using Oxide.Game.Rust.Cui; using UnityEngine; namespace Oxide.Plugins { [Info("TeamShare", "Uzumi & NordicRust", "1.4.0")] [Description("Allows automatic team auth on codelocks, turrets, and tool cupboards via the /share command. Team blueprint sharing follows Operating Mode and can be disabled in config; when Use Permissions is on, sharing requires teamshare.use and teamshare.blueprint.")] public class TeamShare : RustPlugin { [PluginReference] private Plugin WelcomeController; [PluginReference] private Plugin NoEscape; [PluginReference] private Plugin Clans; // Some servers run Clans Reborn under this plugin name. [PluginReference] private Plugin ClansReborn; private class Configuration { [JsonProperty("Use Permissions")] public bool UsePermissions = true; [JsonProperty("Using WelcomeController")] public bool UsingWelcomeController = false; [JsonProperty("Debug Mode")] public bool DebugMode = false; [JsonProperty("Messages")] public Dictionary Messages = new Dictionary { ["NoPermission"] = "You don't have permission to use this command.", ["InvalidCode"] = "Invalid code. Please enter a 4-digit numerical code.", ["CodeSet"] = "Code set to: {0}", ["GuestCodeSet"] = "Guest code set to: {0}", ["NoCodeSet"] = "You have not set any code. Use the UI to set a code.", ["CodeAutoLocked"] = "Code lock placed with code {0}.", ["CodeAutoLockedWithGuest"] = "Code lock placed with code {0} and guest code {1}.", ["CupboardAuthorized"] = "{0} authorized on the Tool Cupboard", ["NoEscape.RaidBlocked"] = "Cannot set codes during raid block.", ["NoEscape.CombatBlocked"] = "Cannot set codes during combat block.", ["NoGuestCodeSet"] = "Please enter a guest code first.", ["AuthedBackward"] = "Authed \"{0}\" to {1} codelocks, {2} turrets and {3} TC's", ["BlueprintSharedSharer"] = "You shared \"{0}\" with {1} teammate(s).", ["BlueprintReceivedReceiver"] = "{0} shared \"{1}\" with the team.", ["BlueprintBulkReceived"] = "Team share: you received {0} blueprint(s) from {1}.", ["BlueprintBulkFromYourLibrary"] = "Team share: {0} received {1} blueprint(s) from your library." }; [JsonProperty("UI Settings")] public UISettings UISettings = new UISettings(); [JsonProperty("Commands Settings")] public CommandsSettings CommandsSettings = new CommandsSettings(); [JsonProperty("AddonName for WelcomeController")] public string AddonName = "TeamShare"; [JsonProperty("NoEscape Integration")] public NoEscapeIntegrationSettings NoEscapeIntegration = new NoEscapeIntegrationSettings(); [JsonProperty("Operating Mode")] public int OperatingMode = 1; [JsonProperty("Blueprint team sharing enabled")] public bool BlueprintTeamSharingEnabled = true; [JsonProperty("Blueprint clan sharing enabled (Clans Reborn)")] public bool BlueprintClanSharingEnabled = true; [JsonProperty("Blueprint share from research items (study action)")] public bool BlueprintShareFromItems = true; [JsonProperty("Blueprint share from tech tree unlocks")] public bool BlueprintShareFromTechTree = true; [JsonProperty("Blueprint share blocked item short names")] public List BlueprintShareBlockedShortNames = new List(); [JsonProperty("Blueprint library resync delay seconds after learn")] public float BlueprintLibraryResyncDelaySeconds = 3f; } public class CommandsSettings { [JsonProperty("Open UI Command")] public string OpenUI = "teamui"; [JsonProperty("Share Command")] public string Share = "share"; } public class UISettings { public string UIBackgroundColor { get; set; } = "0 0 0 0.6"; public string UIPrimaryColor { get; set; } = "1 0.4 0 0.5"; public string UIDangerButtonColor { get; set; } = "1 0 0 1"; [JsonProperty("UIToggleOnColor")] public string UIToggleOnColor { get; set; } = "0.15 0.6 0.15 1"; [JsonProperty("UIToggleOffColor")] public string UIToggleOffColor { get; set; } = "0.6 0.15 0.15 1"; [JsonProperty("Animated Buttons")] public bool AnimatedButtons = true; } public class NoEscapeIntegrationSettings { [JsonProperty("Block During Raid Block")] public bool BlockDuringRaidBlock = true; [JsonProperty("Block During Combat Block")] public bool BlockDuringCombatBlock = true; } private const int OPERATING_MODE_PER_PLACEMENT = 1; private const int OPERATING_MODE_BACKWARD_AUTH = 2; private class EntityCache { public readonly HashSet AutoTurrets = new HashSet(); public readonly HashSet BuildingPrivlidges = new HashSet(); public readonly HashSet CodeLocks = new HashSet(); } private class PlayerData { public bool enabledCodelock { get; set; } = true; public bool enabledCupboard { get; set; } = true; public bool enabledTurret { get; set; } = true; public bool enabledQuiet { get; set; } = false; public bool enabledGuestCode { get; set; } = false; } private const string PERMISSION_USE = "teamshare.use"; private const string PERMISSION_CODELOCK = "teamshare.codelock"; private const string PERMISSION_CUPBOARD = "teamshare.cupboard"; private const string PERMISSION_TURRET = "teamshare.turret"; private const string PERMISSION_GUEST = "teamshare.guest"; private const string PERMISSION_BLUEPRINT = "teamshare.blueprint"; private DynamicConfigFile _playerCodesData; private DynamicConfigFile _playerGuestCodesData; private DynamicConfigFile _playerDataFile; private static Configuration _pluginConfig; private Dictionary _playerCodes = new Dictionary(); private Dictionary _playerGuestCodes = new Dictionary(); private Dictionary _playerData = new Dictionary(); private Dictionary playerUIs = new Dictionary(); private Dictionary playerTempCodes = new Dictionary(); private Dictionary playerTempGuestCodes = new Dictionary(); private Dictionary playerCodeRevealed = new Dictionary(); private Dictionary playerGuestCodeRevealed = new Dictionary(); private Dictionary codeChangedTimers = new Dictionary(); private Dictionary guestCodeChangedTimers = new Dictionary(); private Dictionary animationTimers = new Dictionary(); private UISettings uiSettings; private readonly Dictionary _playerEntities = new Dictionary(); private DynamicConfigFile _blueprintTrackingFile; // receiverId -> (sharerId -> blueprintIds) private Dictionary>> _blueprintTeamSharedByPlayer = new Dictionary>>(); private bool _blueprintTrackingDirty; private readonly Dictionary _blueprintLibraryResyncTimers = new Dictionary(); // receiverId -> (sharerId -> blueprintIds) private Dictionary>> _blueprintClanSharedByPlayer = new Dictionary>>(); private DynamicConfigFile _blueprintClanTrackingFile; private bool _blueprintClanTrackingDirty; private readonly Dictionary> _clanMembersByTag = new Dictionary>(); private class TeamBlueprintTrackingSerialized { public Dictionary>> TeamSharedBlueprintIdsByReceiver = new Dictionary>>(); } // Backward compatible loader for old tracking format (receiverId -> blueprintIds). private class TeamBlueprintTrackingSerializedV1 { public Dictionary> TeamSharedBlueprintIdsByPlayer = new Dictionary>(); } private class ClanBlueprintTrackingSerialized { public Dictionary>> ClanSharedBlueprintIdsByReceiver = new Dictionary>>(); } private class ToggleOption { public string Label; public string Command; public bool IsOn; public string Permission; } #region Hooks and Initialization void Init() { LoadConfigData(); EnsureConfigurationDefaults(); LoadPlayerData(); permission.RegisterPermission(PERMISSION_USE, this); permission.RegisterPermission(PERMISSION_CODELOCK, this); permission.RegisterPermission(PERMISSION_CUPBOARD, this); permission.RegisterPermission(PERMISSION_TURRET, this); permission.RegisterPermission(PERMISSION_GUEST, this); permission.RegisterPermission(PERMISSION_BLUEPRINT, this); lang.RegisterMessages(_pluginConfig.Messages, this); uiSettings = _pluginConfig.UISettings; HandleCommands(); LoadClanBlueprintTracking(); RebuildClanMemberCache(); if (!BlueprintSharingHooksEnabled()) { Unsubscribe(nameof(OnItemAction)); Unsubscribe(nameof(OnBlueprintLearned)); Unsubscribe(nameof(OnTechTreeNodeUnlock)); Unsubscribe(nameof(OnTechTreeNodeUnlocked)); Unsubscribe(nameof(OnPlayerStudyBlueprint)); } else { if (!_pluginConfig.BlueprintShareFromItems) { Unsubscribe(nameof(OnItemAction)); Unsubscribe(nameof(OnPlayerStudyBlueprint)); } if (!_pluginConfig.BlueprintShareFromTechTree) { Unsubscribe(nameof(OnBlueprintLearned)); Unsubscribe(nameof(OnTechTreeNodeUnlock)); Unsubscribe(nameof(OnTechTreeNodeUnlocked)); } } Interface.CallHook("OnWCRequestColors", _pluginConfig.AddonName); if (_pluginConfig.DebugMode) { Puts("TeamShare plugin initialized."); } } private void LoadConfigData() { try { _pluginConfig = Config.ReadObject(); if (_pluginConfig == null) throw new Exception(); if (_pluginConfig.DebugMode) { Puts("Configuration loaded successfully."); } } catch { PrintError("Configuration file is corrupt or missing. Creating new configuration."); LoadDefaultConfig(); } } private void EnsureConfigurationDefaults() { if (_pluginConfig.Messages == null) _pluginConfig.Messages = new Dictionary(); foreach (var kv in new Configuration().Messages) { if (!_pluginConfig.Messages.ContainsKey(kv.Key)) _pluginConfig.Messages[kv.Key] = kv.Value; } } protected override void LoadDefaultConfig() { _pluginConfig = new Configuration(); SaveConfig(); if (_pluginConfig.DebugMode) { Puts("Default configuration loaded."); } } protected override void SaveConfig() { Config.WriteObject(_pluginConfig, true); if (_pluginConfig.DebugMode) { Puts("Configuration saved."); } } private void LoadPlayerData() { _playerCodesData = Interface.Oxide.DataFileSystem.GetDatafile("TeamShare/PlayerCodes"); _playerGuestCodesData = Interface.Oxide.DataFileSystem.GetDatafile("TeamShare/PlayerGuestCodes"); _playerDataFile = Interface.Oxide.DataFileSystem.GetDatafile("TeamShare/PlayerData"); try { _playerCodes = _playerCodesData.ReadObject>(); if (_pluginConfig.DebugMode) { Puts("Player codes loaded."); } } catch { _playerCodes = new Dictionary(); PrintError("Failed to load PlayerCodes data. Initialized empty dictionary."); } try { _playerGuestCodes = _playerGuestCodesData.ReadObject>(); if (_pluginConfig.DebugMode) { Puts("Player guest codes loaded."); } } catch { _playerGuestCodes = new Dictionary(); PrintError("Failed to load PlayerGuestCodes data. Initialized empty dictionary."); } try { _playerData = _playerDataFile.ReadObject>(); if (_pluginConfig.DebugMode) { Puts("Player data loaded."); } } catch { _playerData = new Dictionary(); PrintError("Failed to load PlayerData. Initialized empty dictionary."); } _blueprintTrackingFile = Interface.Oxide.DataFileSystem.GetDatafile("TeamShare/BlueprintTracking"); try { _blueprintTeamSharedByPlayer = new Dictionary>>(); // Try v2 format first. var raw = _blueprintTrackingFile.ReadObject() ?? new TeamBlueprintTrackingSerialized(); if (raw.TeamSharedBlueprintIdsByReceiver != null && raw.TeamSharedBlueprintIdsByReceiver.Count > 0) { foreach (var receiverKvp in raw.TeamSharedBlueprintIdsByReceiver) { var perSharer = new Dictionary>(); if (receiverKvp.Value != null) { foreach (var sharerKvp in receiverKvp.Value) { perSharer[sharerKvp.Key] = new HashSet(sharerKvp.Value ?? new List()); } } _blueprintTeamSharedByPlayer[receiverKvp.Key] = perSharer; } } else { // Fallback to v1 format. We store under sharerId "*" since we do not know the actual sharer. var rawV1 = _blueprintTrackingFile.ReadObject() ?? new TeamBlueprintTrackingSerializedV1(); if (rawV1.TeamSharedBlueprintIdsByPlayer != null) { foreach (var kv in rawV1.TeamSharedBlueprintIdsByPlayer) { _blueprintTeamSharedByPlayer[kv.Key] = new Dictionary> { ["*"] = new HashSet(kv.Value ?? new List()) }; } } } if (_pluginConfig.DebugMode) { Puts("Blueprint tracking data loaded."); } } catch { _blueprintTeamSharedByPlayer = new Dictionary>>(); PrintError("Failed to load TeamShare blueprint tracking. Initialized empty."); } } private void SavePlayerData() { _playerCodesData.WriteObject(_playerCodes); _playerGuestCodesData.WriteObject(_playerGuestCodes); _playerDataFile.WriteObject(_playerData); SaveBlueprintTracking(); SaveClanBlueprintTracking(); if (_pluginConfig.DebugMode) { Puts("Player data saved."); } } private void LoadClanBlueprintTracking() { _blueprintClanTrackingFile = Interface.Oxide.DataFileSystem.GetDatafile("TeamShare/ClanBlueprintTracking"); try { _blueprintClanSharedByPlayer = new Dictionary>>(); var raw = _blueprintClanTrackingFile.ReadObject() ?? new ClanBlueprintTrackingSerialized(); if (raw.ClanSharedBlueprintIdsByReceiver != null && raw.ClanSharedBlueprintIdsByReceiver.Count > 0) { foreach (var receiverKvp in raw.ClanSharedBlueprintIdsByReceiver) { var perSharer = new Dictionary>(); if (receiverKvp.Value != null) { foreach (var sharerKvp in receiverKvp.Value) { perSharer[sharerKvp.Key] = new HashSet(sharerKvp.Value ?? new List()); } } _blueprintClanSharedByPlayer[receiverKvp.Key] = perSharer; } } if (_pluginConfig.DebugMode) Puts("Clan blueprint tracking data loaded."); } catch { _blueprintClanSharedByPlayer = new Dictionary>>(); PrintError("Failed to load Clan blueprint tracking. Initialized empty."); } } private HashSet FetchClanMembersByTag(string tag) { var clansPlugin = GetClansPlugin(); if (clansPlugin == null) return new HashSet(); var clan = clansPlugin.Call("GetClan", tag); if (clan == null) return new HashSet(); var membersToken = clan["members"]; if (membersToken == null) return new HashSet(); var set = new HashSet(); if (membersToken is JArray arr) { foreach (var t in arr) { if (t == null) continue; if (ulong.TryParse(t.ToString(), out var id)) set.Add(id); } } return set; } private void RebuildClanMemberCache() { _clanMembersByTag.Clear(); var clansPlugin = GetClansPlugin(); if (clansPlugin == null) return; var tags = clansPlugin.Call("GetAllClans"); if (tags == null) return; foreach (var t in tags) { var tag = t?.ToString(); if (string.IsNullOrEmpty(tag)) continue; _clanMembersByTag[tag] = FetchClanMembersByTag(tag); } } private PlayerData GetPlayerData(ulong userID) { if (!_playerData.ContainsKey(userID)) { _playerData.Add(userID, new PlayerData()); SavePlayerData(); if (_pluginConfig.DebugMode) { Puts($"Created new PlayerData for UserID: {userID}"); } } return _playerData[userID]; } private bool IsPermitted(BasePlayer player) { if (_pluginConfig.UsePermissions && !permission.UserHasPermission(player.UserIDString, PERMISSION_USE)) { if (_pluginConfig.DebugMode) { Puts($"Player {player.displayName} does not have permission to use TeamShare."); } return false; } return true; } private bool HasBlueprintSharerPermission(ulong sharerUserId) { if (!_pluginConfig.UsePermissions) return true; string id = sharerUserId.ToString(); if (!permission.UserHasPermission(id, PERMISSION_USE)) return false; return permission.UserHasPermission(id, PERMISSION_BLUEPRINT); } private bool CanPlayerInitiateBlueprintSharing(BasePlayer player) { if (player == null) return false; if (!IsPermitted(player)) return false; return HasFeaturePermission(player, PERMISSION_BLUEPRINT); } private bool HasFeaturePermission(BasePlayer player, string perm) { if (!_pluginConfig.UsePermissions) return true; return permission.UserHasPermission(player.UserIDString, perm); } private bool IsValidCode(string code) { bool isValid = code.Length == 4 && int.TryParse(code, out _); if (_pluginConfig.DebugMode) { Puts($"Code validation for '{code}': {isValid}"); } return isValid; } private void SetPlayerCode(ulong playerId, string code) { _playerCodes[playerId] = code; SavePlayerData(); if (_pluginConfig.DebugMode) { Puts($"Set main code for UserID {playerId} to {code}"); } } private string GetPlayerCode(ulong playerId) { string code = _playerCodes.TryGetValue(playerId, out string value) ? value : null; if (_pluginConfig.DebugMode) { Puts($"Retrieved main code for UserID {playerId}: {code}"); } return code; } private void SetPlayerGuestCode(ulong playerId, string code) { _playerGuestCodes[playerId] = code; SavePlayerData(); if (_pluginConfig.DebugMode) { Puts($"Set guest code for UserID {playerId} to {code}"); } } private string GetPlayerGuestCode(ulong playerId) { string code = _playerGuestCodes.TryGetValue(playerId, out string value) ? value : null; if (_pluginConfig.DebugMode) { Puts($"Retrieved guest code for UserID {playerId}: {code}"); } return code; } [HookMethod("IsUsingPlugin")] private bool IsUsingPlugin(string pluginName) { bool isUsing = pluginName.Equals(_pluginConfig.AddonName, StringComparison.OrdinalIgnoreCase); if (_pluginConfig.DebugMode) { Puts($"IsUsingPlugin check for '{pluginName}': {isUsing}"); } return isUsing; } [HookMethod("OnWCRequestedUIPanel")] private void OnWCRequestedUIPanel(BasePlayer player, string panelName, string neededPlugin) { if (!neededPlugin.Equals(_pluginConfig.AddonName, StringComparison.OrdinalIgnoreCase)) return; if (_pluginConfig.DebugMode) { Puts($"Received UI panel request '{panelName}' from player {player.displayName}."); } CreateUI(player, panelName); } void OnWCRequestColors(string pluginName) { if (!pluginName.Equals(_pluginConfig.AddonName, StringComparison.OrdinalIgnoreCase)) return; if (_pluginConfig.DebugMode) { Puts($"Requesting theme colors for plugin '{pluginName}'."); } Interface.CallHook("WCSendColors", ConvertToDictionary(uiSettings), pluginName); } void OnWCSentThemeColors(List pluginNames, Dictionary themeColors) { if (!pluginNames.Contains(_pluginConfig.AddonName)) return; if (_pluginConfig.DebugMode) { Puts($"Received theme colors for plugin '{_pluginConfig.AddonName}'."); } uiSettings.UIBackgroundColor = themeColors.ContainsKey("BackgroundColor") ? themeColors["BackgroundColor"] : uiSettings.UIBackgroundColor; uiSettings.UIPrimaryColor = themeColors.ContainsKey("PrimaryButtonColor") ? themeColors["PrimaryButtonColor"] : uiSettings.UIPrimaryColor; uiSettings.UIDangerButtonColor = themeColors.ContainsKey("SecondaryButtonColor") ? themeColors["SecondaryButtonColor"] : uiSettings.UIDangerButtonColor; SaveConfig(); } private static Dictionary ConvertToDictionary(UISettings uiSettings) { var dictionary = new Dictionary(); foreach (var property in typeof(UISettings).GetProperties()) { string key = property.Name; string value = property.GetValue(uiSettings)?.ToString(); dictionary[key] = value; } return dictionary; } private void HandleCommands() { cmd.AddChatCommand(_pluginConfig.CommandsSettings.OpenUI, this, OpenTeamShareUI); cmd.AddChatCommand(_pluginConfig.CommandsSettings.Share, this, OpenTeamShareUI); if (_pluginConfig.DebugMode) { Puts($"Chat commands registered: /{_pluginConfig.CommandsSettings.OpenUI}, /{_pluginConfig.CommandsSettings.Share}"); } } private void OnServerSave() { SavePlayerData(); } private void Unload() { foreach (var kvp in codeChangedTimers.ToList()) { kvp.Value?.Destroy(); } codeChangedTimers.Clear(); foreach (var kvp in guestCodeChangedTimers.ToList()) { kvp.Value?.Destroy(); } guestCodeChangedTimers.Clear(); foreach (var kvp in animationTimers.ToList()) { kvp.Value?.Destroy(); } animationTimers.Clear(); foreach (var kvp in _blueprintLibraryResyncTimers.ToList()) { kvp.Value?.Destroy(); } _blueprintLibraryResyncTimers.Clear(); foreach (BasePlayer player in BasePlayer.activePlayerList) { DestroyUI(player); } SavePlayerData(); if (_pluginConfig?.DebugMode == true) { Puts("TeamShare plugin unloaded. All UIs and timers destroyed, data saved."); } } private void OnServerInitialized() { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; foreach (var serverEntity in BaseNetworkable.serverEntities) { if (serverEntity == null || serverEntity.IsDestroyed) continue; if (serverEntity is AutoTurret autoTurret) { if (autoTurret.OwnerID == 0) continue; if (!_pluginConfig.UsePermissions || permission.UserHasPermission(autoTurret.OwnerID.ToString(), PERMISSION_USE)) AddEntityToCache(autoTurret.OwnerID, autoTurret); continue; } if (serverEntity is BuildingPrivlidge buildingPrivlidge) { if (buildingPrivlidge.OwnerID == 0) continue; if (!_pluginConfig.UsePermissions || permission.UserHasPermission(buildingPrivlidge.OwnerID.ToString(), PERMISSION_USE)) AddEntityToCache(buildingPrivlidge.OwnerID, buildingPrivlidge); continue; } if (serverEntity is CodeLock codeLock) { var parent = codeLock.GetParentEntity(); ulong ownerId = codeLock.OwnerID != 0 ? codeLock.OwnerID : parent?.OwnerID ?? 0; if (ownerId == 0) continue; if (!_pluginConfig.UsePermissions || permission.UserHasPermission(ownerId.ToString(), PERMISSION_USE)) AddEntityToCache(ownerId, codeLock); } } if (_pluginConfig.DebugMode) Puts("Mode 2: Entity cache rebuilt from server entities."); } #endregion #region Entity Deployment Hooks void OnEntitySpawned(BaseNetworkable entity) { if (entity == null) return; if (entity is CodeLock codelock) { HandleCodeLockDeployment(codelock); } else if (entity is BuildingPrivlidge cupboard) { HandleCupboardDeployment(cupboard); } else if (entity is AutoTurret turret) { HandleAutoTurretDeployment(turret); } } private void HandleCodeLockDeployment(CodeLock codelock) { BasePlayer player = GetPermittedOwner(codelock); if (player == null || !IsPermitted(player)) return; if (!HasFeaturePermission(player, PERMISSION_CODELOCK)) return; if (_pluginConfig.DebugMode) { Puts($"Handling CodeLock deployment by {player.displayName} (UserID: {player.UserIDString})"); } if (NoEscape != null) { object raidBlockedResult = NoEscape.Call("IsRaidBlocked", player); if (_pluginConfig.NoEscapeIntegration.BlockDuringRaidBlock && raidBlockedResult is bool isRaidBlocked && isRaidBlocked) { player.ChatMessage(Lang("NoEscape.RaidBlocked", player.UserIDString)); if (_pluginConfig.DebugMode) { Puts($"Raid block active. Deployment of CodeLock by {player.displayName} blocked."); } return; } object combatBlockedResult = NoEscape.Call("IsCombatBlocked", player); if (_pluginConfig.NoEscapeIntegration.BlockDuringCombatBlock && combatBlockedResult is bool isCombatBlocked && isCombatBlocked) { player.ChatMessage(Lang("NoEscape.CombatBlocked", player.UserIDString)); if (_pluginConfig.DebugMode) { Puts($"Combat block active. Deployment of CodeLock by {player.displayName} blocked."); } return; } } PlayerData pData = GetPlayerData(player.userID); if (pData.enabledCodelock) { string code = GetPlayerCode(player.userID); if (!string.IsNullOrEmpty(code)) { codelock.code = code; codelock.hasCode = true; codelock.SetFlag(BaseEntity.Flags.Locked, true); codelock.whitelistPlayers.Clear(); SetEntityWhitelist(player, codelock); if (pData.enabledGuestCode && HasFeaturePermission(player, PERMISSION_GUEST)) { string guestCode = GetPlayerGuestCode(player.userID); if (!string.IsNullOrEmpty(guestCode)) { codelock.guestCode = guestCode; codelock.hasGuestCode = true; codelock.guestPlayers.Clear(); } else { codelock.hasGuestCode = false; } } else { codelock.hasGuestCode = false; } codelock.SendNetworkUpdateImmediate(); if (!pData.enabledQuiet) { List authorizedNames = GetAuthorizedNames(GetTeamMembers(player)); string message = pData.enabledGuestCode && codelock.hasGuestCode ? $"Code lock placed with code \"{code}\" and guest code \"{codelock.guestCode}\" authed {string.Join(", ", authorizedNames)}" : $"Code lock placed with code \"{code}\" authed {string.Join(", ", authorizedNames)}"; SendChatMessage(player, message); } } else { if (!pData.enabledQuiet) SendChatMessage(player, Lang("NoCodeSet", player.UserIDString)); } } if (_pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH) { AddEntityToCache(player.userID, codelock); } if (_pluginConfig.DebugMode) { Puts($"CodeLock deployment handled for {player.displayName}."); } } private void HandleCupboardDeployment(BuildingPrivlidge cupboard) { BasePlayer player = GetPermittedOwner(cupboard); if (player == null || !IsPermitted(player)) return; if (!HasFeaturePermission(player, PERMISSION_CUPBOARD)) return; if (_pluginConfig.DebugMode) { Puts($"Handling Tool Cupboard deployment by {player.displayName} (UserID: {player.UserIDString})"); } PlayerData pData = GetPlayerData(player.userID); if (pData.enabledCupboard) { SetEntityWhitelist(player, cupboard); cupboard.SendNetworkUpdateImmediate(); if (!pData.enabledQuiet) { List authorizedNames = GetAuthorizedNames(GetTeamMembers(player)); string authedMessage = $"Authorized {string.Join(", ", authorizedNames)} on the Tool Cupboard"; SendChatMessage(player, authedMessage); } } if (_pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH) { AddEntityToCache(player.userID, cupboard); } if (_pluginConfig.DebugMode) { Puts($"Tool Cupboard deployment handled for {player.displayName}."); } } private void HandleAutoTurretDeployment(AutoTurret turret) { BasePlayer player = GetPermittedOwner(turret); if (player == null || !IsPermitted(player)) return; if (!HasFeaturePermission(player, PERMISSION_TURRET)) return; if (_pluginConfig.DebugMode) { Puts($"Handling Auto Turret deployment by {player.displayName} (UserID: {player.UserIDString})"); } PlayerData pData = GetPlayerData(player.userID); if (pData.enabledTurret) { SetEntityWhitelist(player, turret); turret.SendNetworkUpdateImmediate(); if (!pData.enabledQuiet) { List authorizedNames = GetAuthorizedNames(GetTeamMembers(player)); string authedMessage = $"Authorized {string.Join(", ", authorizedNames)} on the Auto Turret"; SendChatMessage(player, authedMessage); } } if (_pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH) { AddEntityToCache(player.userID, turret); } if (_pluginConfig.DebugMode) { Puts($"Auto Turret deployment handled for {player.displayName}."); } } private BasePlayer GetPermittedOwner(BaseEntity entity) { if (entity == null) return null; BasePlayer player = BasePlayer.FindByID(entity.OwnerID); if (player == null || !IsPermitted(player)) { if (_pluginConfig.DebugMode) { Puts($"Entity owner not found or lacks permission."); } return null; } if (_pluginConfig.DebugMode) { Puts($"Permitted owner found: {player.displayName} (UserID: {player.UserIDString})"); } return player; } private List GetTeamMembers(BasePlayer player) { if (player == null) return new List(); List members = new List { player.userID }; if (player.currentTeam != 0) { RelationshipManager.PlayerTeam team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam); if (team != null) { foreach (ulong memberId in team.members) { if (memberId != player.userID) members.Add(memberId); } } } if (_pluginConfig.DebugMode) { Puts($"Team members for {player.displayName}: {string.Join(", ", members)}"); } return members; } private List GetAuthorizedNames(List memberIds) { List names = new List(); foreach (ulong id in memberIds) { string name = covalence.Players.FindPlayerById(id.ToString())?.Name ?? "Unknown"; names.Add($"\"{name}\""); } return names; } private void SetEntityWhitelist(BasePlayer player, CodeLock codelock) { List teamMembers = GetTeamMembers(player); foreach (ulong memberId in teamMembers) { codelock.whitelistPlayers.Add(memberId); } if (_pluginConfig.DebugMode) { foreach (ulong memberId in teamMembers) { Puts($"UserID {memberId} authorized on CodeLock by {player.displayName}."); } } } private void SetEntityWhitelist(BasePlayer player, BuildingPrivlidge cupboard) { List teamMembers = GetTeamMembers(player); foreach (ulong memberId in teamMembers) { cupboard.authorizedPlayers.Add(memberId); } if (_pluginConfig.DebugMode) { foreach (ulong memberId in teamMembers) { Puts($"UserID {memberId} authorized on Tool Cupboard by {player.displayName}."); } } } private void SetEntityWhitelist(BasePlayer player, AutoTurret turret) { List teamMembers = GetTeamMembers(player); foreach (ulong memberId in teamMembers) { turret.authorizedPlayers.Add(memberId); } if (_pluginConfig.DebugMode) { foreach (ulong memberId in teamMembers) { Puts($"UserID {memberId} authorized on Auto Turret by {player.displayName}."); } } } #region Team blueprint sharing private bool BlueprintSharingHooksEnabled() { if (!_pluginConfig.BlueprintTeamSharingEnabled) return false; return _pluginConfig.BlueprintShareFromItems || _pluginConfig.BlueprintShareFromTechTree; } private bool ClanBlueprintSharingEnabled() { if (!_pluginConfig.BlueprintClanSharingEnabled) return false; var clansPlugin = GetClansPlugin(); return clansPlugin != null && clansPlugin.IsLoaded; } private Plugin GetClansPlugin() { if (ClansReborn != null && ClansReborn.IsLoaded) return ClansReborn; if (Clans != null && Clans.IsLoaded) return Clans; return null; } private bool IsClanMember(ulong playerId, ulong otherId) { var clansPlugin = GetClansPlugin(); return clansPlugin != null && clansPlugin.Call("IsClanMember", playerId, otherId); } private List GetClanMembers(ulong playerId) { var clansPlugin = GetClansPlugin(); if (clansPlugin == null) return new List(); var members = clansPlugin.Call>("GetClanMembers", playerId); if (members == null) return new List(); var result = new List(); foreach (var idStr in members) { if (ulong.TryParse(idStr, out var id)) result.Add(id); } return result; } private bool PlayerHasClanmatesForBlueprintShare(ulong playerId) { var members = GetClanMembers(playerId); return members.Count > 1; } private List GetClanmateIdsForBlueprintShare(ulong sharerId) { return GetClanMembers(sharerId).Where(id => id != sharerId).ToList(); } private void HandleBlueprintTeamInviteAccepted(RelationshipManager.PlayerTeam playerTeam, BasePlayer joiningPlayer) { if (!BlueprintSharingHooksEnabled() || playerTeam == null || joiningPlayer == null) return; ulong joinerId = joiningPlayer.userID; timer.Once(1f, () => { if (playerTeam.members == null || !playerTeam.members.Contains(joinerId)) return; foreach (ulong otherId in playerTeam.members) { if (otherId == joinerId) continue; ShareAllBlueprintsFromTo(otherId, joinerId); ShareAllBlueprintsFromTo(joinerId, otherId); } FlushBlueprintTrackingIfDirty(); }); } private void HandleClanMemberJoined(ulong joiningId, IEnumerable existingMemberIds) { if (!ClanBlueprintSharingEnabled()) return; if (existingMemberIds == null) return; foreach (ulong otherId in existingMemberIds) { if (otherId == joiningId) continue; ShareAllBlueprintsFromToClan(otherId, joiningId); ShareAllBlueprintsFromToClan(joiningId, otherId); } FlushClanBlueprintTrackingIfDirty(); } private void HandleClanMemberGone(ulong leavingId, IEnumerable remainingMemberIds) { if (!ClanBlueprintSharingEnabled()) return; // Remove blueprints the leaving player received from clanmates. RemoveTrackedClanBlueprintsFromPlayer(leavingId); // Remove blueprints remaining members received from the leaving player. if (remainingMemberIds != null) { foreach (ulong receiverId in remainingMemberIds) { if (receiverId == leavingId) continue; RemoveTrackedClanBlueprintsFromPlayer(receiverId, leavingId); } } FlushClanBlueprintTrackingIfDirty(); } private void ScheduleBlueprintLibrarySyncToTeammates(ulong learnerId) { if (!BlueprintSharingHooksEnabled()) return; var learner = BasePlayer.FindByID(learnerId); if (learner != null && !CanPlayerInitiateBlueprintSharing(learner)) return; if (!PlayerHasTeammatesForBlueprintShare(learnerId)) return; float delay = _pluginConfig.BlueprintLibraryResyncDelaySeconds; if (delay < 0f) delay = 0f; if (_blueprintLibraryResyncTimers.TryGetValue(learnerId, out var existing)) { existing?.Destroy(); _blueprintLibraryResyncTimers.Remove(learnerId); } ulong idCopy = learnerId; _blueprintLibraryResyncTimers[learnerId] = timer.Once(delay, () => { _blueprintLibraryResyncTimers.Remove(idCopy); NextTick(() => RunBlueprintLibrarySyncFromPlayerToAllTeammates(idCopy)); }); } private void RunBlueprintLibrarySyncFromPlayerToAllTeammates(ulong sharerId) { if (!BlueprintSharingHooksEnabled()) return; if (!HasBlueprintSharerPermission(sharerId)) return; var team = RelationshipManager.ServerInstance?.FindPlayersTeam(sharerId); if (team?.members == null || team.members.Count < 2) return; foreach (ulong teammateId in team.members) { if (teammateId == sharerId) continue; ShareAllBlueprintsFromTo(sharerId, teammateId); } FlushBlueprintTrackingIfDirty(); } private void CancelBlueprintLibraryResyncTimer(ulong playerId) { if (!_blueprintLibraryResyncTimers.TryGetValue(playerId, out var t)) return; t?.Destroy(); _blueprintLibraryResyncTimers.Remove(playerId); } private void HandleBlueprintPlayerLeftTeam(ulong leavingPlayerId, IList lastKnownTeamMembers = null) { // Remove blueprints the leaving player received from the team. RemoveTrackedTeamBlueprintsFromPlayer(leavingPlayerId); // Also remove blueprints other players received from the leaving player. if (lastKnownTeamMembers == null || lastKnownTeamMembers.Count == 0) return; foreach (ulong receiverId in lastKnownTeamMembers) { if (receiverId == leavingPlayerId) continue; RemoveTrackedTeamBlueprintsFromPlayer(receiverId, leavingPlayerId); } } private void ShareAllBlueprintsFromTo(ulong sharerId, ulong receiverId) { if (sharerId == receiverId) return; if (!HasBlueprintSharerPermission(sharerId)) return; var filtered = GetFilteredUnlockedBlueprintIdsForPlayer(sharerId); if (filtered.Count == 0) return; var queue = new List(); bool track = _pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH; foreach (int blueprintId in filtered) { QueueBlueprintUnlockForTeam(sharerId, receiverId, blueprintId, queue, track); } if (queue.Count > 0) { int n = ProcessQueuedBlueprintUnlocks(receiverId, queue); if (n > 0) { string sharerName = ResolvePlayerDisplayName(sharerId); string receiverName = ResolvePlayerDisplayName(receiverId); var receiver = BasePlayer.FindByID(receiverId); if (receiver != null && receiver.IsConnected) SendChatMessage(receiver, Lang("BlueprintBulkReceived", receiver.UserIDString), n, sharerName); var sharer = BasePlayer.FindByID(sharerId); if (sharer != null && sharer.IsConnected) SendChatMessage(sharer, Lang("BlueprintBulkFromYourLibrary", sharer.UserIDString), receiverName, n); } } } private void ShareAllBlueprintsFromToClan(ulong sharerId, ulong receiverId) { if (!ClanBlueprintSharingEnabled()) return; if (sharerId == receiverId) return; if (!IsClanMember(sharerId, receiverId)) return; if (!HasBlueprintSharerPermission(sharerId)) return; var filtered = GetFilteredUnlockedBlueprintIdsForPlayer(sharerId); if (filtered.Count == 0) return; var queue = new List(); bool track = _pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH; foreach (int blueprintId in filtered) { QueueBlueprintUnlockForClan(sharerId, receiverId, blueprintId, queue, track); } if (queue.Count > 0) { int n = ProcessQueuedBlueprintUnlocks(receiverId, queue); if (n > 0) { string sharerName = ResolvePlayerDisplayName(sharerId); string receiverName = ResolvePlayerDisplayName(receiverId); var receiver = BasePlayer.FindByID(receiverId); if (receiver != null && receiver.IsConnected) SendChatMessage(receiver, Lang("BlueprintBulkReceived", receiver.UserIDString), n, sharerName); var sharer = BasePlayer.FindByID(sharerId); if (sharer != null && sharer.IsConnected) SendChatMessage(sharer, Lang("BlueprintBulkFromYourLibrary", sharer.UserIDString), receiverName, n); } FlushClanBlueprintTrackingIfDirty(); } } private HashSet GetMergedUnlockedItemIds(ulong playerId) { var ids = new HashSet(); var online = BasePlayer.FindByID(playerId); if (online?.PersistantPlayerInfo?.unlockedItems != null) { foreach (int id in online.PersistantPlayerInfo.unlockedItems) ids.Add(id); } var persisted = ServerMgr.Instance?.persistance?.GetPlayerInfo(playerId)?.unlockedItems; if (persisted != null) { foreach (int id in persisted) ids.Add(id); } return ids; } private List GetFilteredUnlockedBlueprintIdsForPlayer(ulong userId) { var result = new List(); foreach (int id in GetMergedUnlockedItemIds(userId)) { var def = ItemManager.FindItemDefinition(id); if (def != null && !IsBlueprintShareBlocked(def)) result.Add(id); } return result; } private bool IsBlueprintShareBlocked(ItemDefinition def) { if (def == null) return true; if (_pluginConfig.BlueprintShareBlockedShortNames == null || _pluginConfig.BlueprintShareBlockedShortNames.Count == 0) return false; return _pluginConfig.BlueprintShareBlockedShortNames.Any(s => string.Equals(s, def.shortname, StringComparison.OrdinalIgnoreCase)); } private bool PlayerWouldLearnBlueprint(ulong playerId, int blueprintId) { var unlocked = GetMergedUnlockedItemIds(playerId); if (unlocked.Count == 0) return true; return !unlocked.Contains(blueprintId); } private bool PlayerHasTeammatesForBlueprintShare(ulong playerId) { var team = RelationshipManager.ServerInstance?.FindPlayersTeam(playerId); return team?.members != null && team.members.Count > 1; } private List GetTeammateIdsForBlueprintShare(ulong sharerId) { var list = new List(); var team = RelationshipManager.ServerInstance?.FindPlayersTeam(sharerId); if (team?.members == null) return list; foreach (ulong id in team.members) { if (id != sharerId) list.Add(id); } return list; } private bool SomeoneWillLearnBlueprintFromTeammates(ulong researcherId, ItemDefinition item) { if (item == null) return false; foreach (ulong tid in GetTeammateIdsForBlueprintShare(researcherId)) { if (PlayerWouldLearnBlueprint(tid, item.itemid)) return true; if (item.Blueprint?.additionalUnlocks != null) { foreach (var add in item.Blueprint.additionalUnlocks) { if (PlayerWouldLearnBlueprint(tid, add.itemid)) return true; } } } return false; } private bool QueueBlueprintUnlockForTeam(ulong sharerId, ulong receiverId, int blueprintId, List unlockQueue, bool trackForRemoval) { var online = BasePlayer.FindByID(receiverId); if (online?.PersistantPlayerInfo?.unlockedItems != null && online.PersistantPlayerInfo.unlockedItems.Contains(blueprintId)) return false; var playerInfo = ServerMgr.Instance?.persistance?.GetPlayerInfo(receiverId); if (playerInfo == null) return false; if (playerInfo.unlockedItems == null) playerInfo.unlockedItems = new List(); if (playerInfo.unlockedItems.Contains(blueprintId)) return false; unlockQueue.Add(blueprintId); if (trackForRemoval) RegisterTeamSharedBlueprint(sharerId, receiverId, blueprintId); return true; } private bool QueueBlueprintUnlockForClan(ulong sharerId, ulong receiverId, int blueprintId, List unlockQueue, bool trackForRemoval) { var online = BasePlayer.FindByID(receiverId); if (online?.PersistantPlayerInfo?.unlockedItems != null && online.PersistantPlayerInfo.unlockedItems.Contains(blueprintId)) return false; var playerInfo = ServerMgr.Instance?.persistance?.GetPlayerInfo(receiverId); if (playerInfo == null) return false; if (playerInfo.unlockedItems == null) playerInfo.unlockedItems = new List(); if (playerInfo.unlockedItems.Contains(blueprintId)) return false; unlockQueue.Add(blueprintId); if (trackForRemoval) RegisterClanSharedBlueprint(sharerId, receiverId, blueprintId); return true; } private void RegisterTeamSharedBlueprint(ulong sharerId, ulong receiverId, int blueprintId) { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; string receiverKey = receiverId.ToString(); string sharerKey = sharerId.ToString(); if (!_blueprintTeamSharedByPlayer.TryGetValue(receiverKey, out var perSharer)) { perSharer = new Dictionary>(); _blueprintTeamSharedByPlayer[receiverKey] = perSharer; } if (!perSharer.TryGetValue(sharerKey, out var set)) { set = new HashSet(); perSharer[sharerKey] = set; } set.Add(blueprintId); _blueprintTrackingDirty = true; } private void RegisterClanSharedBlueprint(ulong sharerId, ulong receiverId, int blueprintId) { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; string receiverKey = receiverId.ToString(); string sharerKey = sharerId.ToString(); if (!_blueprintClanSharedByPlayer.TryGetValue(receiverKey, out var perSharer)) { perSharer = new Dictionary>(); _blueprintClanSharedByPlayer[receiverKey] = perSharer; } if (!perSharer.TryGetValue(sharerKey, out var set)) { set = new HashSet(); perSharer[sharerKey] = set; } set.Add(blueprintId); _blueprintClanTrackingDirty = true; } private int ProcessQueuedBlueprintUnlocks(ulong playerId, List unlockQueue) { if (unlockQueue == null || unlockQueue.Count == 0) return 0; var persistance = ServerMgr.Instance?.persistance; if (persistance == null) return 0; var playerInfo = persistance.GetPlayerInfo(playerId); if (playerInfo == null) return 0; if (playerInfo.unlockedItems == null) playerInfo.unlockedItems = new List(); foreach (int bpId in unlockQueue) { if (!playerInfo.unlockedItems.Contains(bpId)) playerInfo.unlockedItems.Add(bpId); } persistance.SetPlayerInfo(playerId, playerInfo); var player = BasePlayer.FindByID(playerId); if (player != null) { foreach (int blueprintId in unlockQueue) { SyncBlueprintUnlockToClient(player, blueprintId); } player.stats.Add("blueprint_studied", unlockQueue.Count); PlayBlueprintShareSuccessEffect(player); } if (_pluginConfig.DebugMode) Puts($"TeamShare: unlocked {unlockQueue.Count} blueprint(s) for player {playerId}."); return unlockQueue.Count; } private static void SyncBlueprintUnlockToClient(BasePlayer player, int blueprintId) { if (player?.PersistantPlayerInfo?.unlockedItems == null) return; if (!player.PersistantPlayerInfo.unlockedItems.Contains(blueprintId)) player.PersistantPlayerInfo.unlockedItems.Add(blueprintId); player.ClientRPC(null, "UnlockedBlueprint", blueprintId); player.SendNetworkUpdateImmediate(); } private static void PlayBlueprintShareSuccessEffect(BasePlayer player) { if (player == null) return; Effect.server.Run("assets/prefabs/deployable/vendingmachine/effects/vending-machine-purchase-human.prefab", player.transform.position); } private void ShareBlueprintWithTeammates(BasePlayer sharer, ItemDefinition item) { if (item == null || sharer == null) return; var targets = GetTeammateIdsForBlueprintShare(sharer.userID); if (targets.Count == 0) return; string itemName = item.displayName?.translated; if (string.IsNullOrEmpty(itemName)) itemName = item.shortname ?? "blueprint"; bool track = _pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH; int teammatesReached = 0; foreach (ulong targetId in targets) { var taskQueue = new List(); if (item.Blueprint?.additionalUnlocks != null) { foreach (var add in item.Blueprint.additionalUnlocks) { QueueBlueprintUnlockForTeam(sharer.userID, targetId, add.itemid, taskQueue, track); } } QueueBlueprintUnlockForTeam(sharer.userID, targetId, item.itemid, taskQueue, track); if (taskQueue.Count == 0) continue; int added = ProcessQueuedBlueprintUnlocks(targetId, taskQueue); if (added <= 0) continue; teammatesReached++; var targetPlayer = BasePlayer.FindByID(targetId); if (targetPlayer != null && targetPlayer.IsConnected) SendChatMessage(targetPlayer, Lang("BlueprintReceivedReceiver", targetPlayer.UserIDString), sharer.displayName, itemName); } if (teammatesReached > 0) SendChatMessage(sharer, Lang("BlueprintSharedSharer", sharer.UserIDString), itemName, teammatesReached); FlushBlueprintTrackingIfDirty(); } private bool TryShareBlueprintWithTeammates(ItemDefinition item, BasePlayer player) { if (item == null || player == null) return false; if (!CanPlayerInitiateBlueprintSharing(player)) return false; if (IsBlueprintShareBlocked(item)) return false; if (!PlayerHasTeammatesForBlueprintShare(player.userID)) return false; if (!SomeoneWillLearnBlueprintFromTeammates(player.userID, item)) return false; ShareBlueprintWithTeammates(player, item); return true; } private bool SomeoneWillLearnBlueprintFromClanmates(ulong researcherId, ItemDefinition item) { if (item == null) return false; foreach (ulong tid in GetClanmateIdsForBlueprintShare(researcherId)) { if (PlayerWouldLearnBlueprint(tid, item.itemid)) return true; if (item.Blueprint?.additionalUnlocks != null) { foreach (var add in item.Blueprint.additionalUnlocks) { if (PlayerWouldLearnBlueprint(tid, add.itemid)) return true; } } } return false; } private void ShareBlueprintWithClanmates(BasePlayer sharer, ItemDefinition item) { if (item == null || sharer == null) return; var targets = GetClanmateIdsForBlueprintShare(sharer.userID); if (targets.Count == 0) return; string itemName = item.displayName?.translated; if (string.IsNullOrEmpty(itemName)) itemName = item.shortname ?? "blueprint"; bool track = _pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH; int clanmatesReached = 0; foreach (ulong targetId in targets) { if (!IsClanMember(sharer.userID, targetId)) continue; var taskQueue = new List(); if (item.Blueprint?.additionalUnlocks != null) { foreach (var add in item.Blueprint.additionalUnlocks) { QueueBlueprintUnlockForClan(sharer.userID, targetId, add.itemid, taskQueue, track); } } QueueBlueprintUnlockForClan(sharer.userID, targetId, item.itemid, taskQueue, track); if (taskQueue.Count == 0) continue; int added = ProcessQueuedBlueprintUnlocks(targetId, taskQueue); if (added <= 0) continue; clanmatesReached++; var targetPlayer = BasePlayer.FindByID(targetId); if (targetPlayer != null && targetPlayer.IsConnected) SendChatMessage(targetPlayer, Lang("BlueprintReceivedReceiver", targetPlayer.UserIDString), sharer.displayName, itemName); } if (clanmatesReached > 0) SendChatMessage(sharer, Lang("BlueprintSharedSharer", sharer.UserIDString), itemName, clanmatesReached); FlushClanBlueprintTrackingIfDirty(); } private bool TryShareBlueprintWithClanmates(ItemDefinition item, BasePlayer player) { if (!ClanBlueprintSharingEnabled()) return false; if (item == null || player == null) return false; if (!CanPlayerInitiateBlueprintSharing(player)) return false; if (IsBlueprintShareBlocked(item)) return false; if (!PlayerHasClanmatesForBlueprintShare(player.userID)) return false; if (!SomeoneWillLearnBlueprintFromClanmates(player.userID, item)) return false; ShareBlueprintWithClanmates(player, item); return true; } private void RemoveTrackedTeamBlueprintsFromPlayer(ulong playerId) { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; string key = playerId.ToString(); if (!_blueprintTeamSharedByPlayer.TryGetValue(key, out var perSharer) || perSharer == null || perSharer.Count == 0) return; var snapshot = new HashSet(); foreach (var kv in perSharer) { if (kv.Value == null) continue; snapshot.UnionWith(kv.Value); } if (snapshot.Count == 0) return; RemoveBlueprintsFromPersistence(playerId, snapshot); _blueprintTeamSharedByPlayer.Remove(key); _blueprintTrackingDirty = true; if (_pluginConfig.DebugMode) Puts($"TeamShare: removed {snapshot.Count} team-shared blueprint(s) from player {playerId}."); FlushBlueprintTrackingIfDirty(); } private void RemoveTrackedTeamBlueprintsFromPlayer(ulong receiverId, ulong sharerId) { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; string receiverKey = receiverId.ToString(); string sharerKey = sharerId.ToString(); if (!_blueprintTeamSharedByPlayer.TryGetValue(receiverKey, out var perSharer) || perSharer == null) return; if (!perSharer.TryGetValue(sharerKey, out var ids) || ids == null || ids.Count == 0) return; var snapshot = new HashSet(ids); RemoveBlueprintsFromPersistence(receiverId, snapshot); perSharer.Remove(sharerKey); if (perSharer.Count == 0) _blueprintTeamSharedByPlayer.Remove(receiverKey); _blueprintTrackingDirty = true; if (_pluginConfig.DebugMode) Puts($"TeamShare: removed {snapshot.Count} team-shared blueprint(s) from player {receiverId} that came from {sharerId}."); FlushBlueprintTrackingIfDirty(); } private void RemoveTrackedClanBlueprintsFromPlayer(ulong playerId) { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; string key = playerId.ToString(); if (!_blueprintClanSharedByPlayer.TryGetValue(key, out var perSharer) || perSharer == null || perSharer.Count == 0) return; var snapshot = new HashSet(); foreach (var kv in perSharer) { if (kv.Value == null) continue; snapshot.UnionWith(kv.Value); } if (snapshot.Count == 0) return; RemoveBlueprintsFromPersistence(playerId, snapshot); _blueprintClanSharedByPlayer.Remove(key); _blueprintClanTrackingDirty = true; FlushClanBlueprintTrackingIfDirty(); } private void RemoveTrackedClanBlueprintsFromPlayer(ulong receiverId, ulong sharerId) { if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; string receiverKey = receiverId.ToString(); string sharerKey = sharerId.ToString(); if (!_blueprintClanSharedByPlayer.TryGetValue(receiverKey, out var perSharer) || perSharer == null) return; if (!perSharer.TryGetValue(sharerKey, out var ids) || ids == null || ids.Count == 0) return; var snapshot = new HashSet(ids); RemoveBlueprintsFromPersistence(receiverId, snapshot); perSharer.Remove(sharerKey); if (perSharer.Count == 0) _blueprintClanSharedByPlayer.Remove(receiverKey); _blueprintClanTrackingDirty = true; FlushClanBlueprintTrackingIfDirty(); } private void RemoveBlueprintsFromPersistence(ulong playerId, HashSet blueprintIds) { if (blueprintIds == null || blueprintIds.Count == 0) return; var persistance = ServerMgr.Instance?.persistance; if (persistance == null) return; var playerInfo = persistance.GetPlayerInfo(playerId); if (playerInfo?.unlockedItems == null) return; var player = BasePlayer.FindByID(playerId); int blueprintsRemoved = 0; foreach (int blueprintId in blueprintIds) { if (!playerInfo.unlockedItems.Contains(blueprintId)) continue; playerInfo.unlockedItems.Remove(blueprintId); blueprintsRemoved++; if (player != null && player.PersistantPlayerInfo != null && player.PersistantPlayerInfo.unlockedItems != null && player.PersistantPlayerInfo.unlockedItems.Contains(blueprintId)) { player.PersistantPlayerInfo.unlockedItems.Remove(blueprintId); } } if (blueprintsRemoved == 0) return; persistance.SetPlayerInfo(playerId, playerInfo); if (player != null) player.SendNetworkUpdateImmediate(); } private void SaveBlueprintTracking() { if (_blueprintTrackingFile == null) return; if (!_blueprintTrackingDirty) return; var serialized = new TeamBlueprintTrackingSerialized(); foreach (var receiverKvp in _blueprintTeamSharedByPlayer) { if (receiverKvp.Value == null || receiverKvp.Value.Count == 0) continue; var perSharer = new Dictionary>(); foreach (var sharerKvp in receiverKvp.Value) { if (sharerKvp.Value == null || sharerKvp.Value.Count == 0) continue; perSharer[sharerKvp.Key] = sharerKvp.Value.ToList(); } if (perSharer.Count > 0) serialized.TeamSharedBlueprintIdsByReceiver[receiverKvp.Key] = perSharer; } _blueprintTrackingFile.WriteObject(serialized); _blueprintTrackingDirty = false; } private void FlushBlueprintTrackingIfDirty() { if (_blueprintTrackingDirty) SaveBlueprintTracking(); } private void SaveClanBlueprintTracking() { if (_blueprintClanTrackingFile == null) return; if (!_blueprintClanTrackingDirty) return; var serialized = new ClanBlueprintTrackingSerialized(); foreach (var receiverKvp in _blueprintClanSharedByPlayer) { if (receiverKvp.Value == null || receiverKvp.Value.Count == 0) continue; var perSharer = new Dictionary>(); foreach (var sharerKvp in receiverKvp.Value) { if (sharerKvp.Value == null || sharerKvp.Value.Count == 0) continue; perSharer[sharerKvp.Key] = sharerKvp.Value.ToList(); } if (perSharer.Count > 0) serialized.ClanSharedBlueprintIdsByReceiver[receiverKvp.Key] = perSharer; } _blueprintClanTrackingFile.WriteObject(serialized); _blueprintClanTrackingDirty = false; } private void FlushClanBlueprintTrackingIfDirty() { if (_blueprintClanTrackingDirty) SaveClanBlueprintTracking(); } void OnItemAction(Item item, string action, BasePlayer player) { if (!_pluginConfig.BlueprintShareFromItems || item == null || player == null) return; if (string.IsNullOrEmpty(action) || !action.Equals("study", StringComparison.OrdinalIgnoreCase)) return; if (item.blueprintTargetDef == null) return; if (TryShareBlueprintWithTeammates(item.blueprintTargetDef, player)) item.Remove(); TryShareBlueprintWithClanmates(item.blueprintTargetDef, player); ScheduleBlueprintLibrarySyncToTeammates(player.userID); } private object OnPlayerStudyBlueprint(BasePlayer player, Item item) { if (!_pluginConfig.BlueprintShareFromItems || item == null || player == null) return null; if (item.blueprintTargetDef != null) { TryShareBlueprintWithTeammates(item.blueprintTargetDef, player); TryShareBlueprintWithClanmates(item.blueprintTargetDef, player); } ScheduleBlueprintLibrarySyncToTeammates(player.userID); return null; } private void HandleTechTreeOrBlueprintLearnedHook(BasePlayer player, ItemDefinition itemDefinition, string hookNameForDebug) { if (player == null || itemDefinition == null) return; if (_pluginConfig.DebugMode) Puts($"TeamShare: {hookNameForDebug} player={player.displayName} item={itemDefinition.shortname}"); if (BlueprintSharingHooksEnabled()) ScheduleBlueprintLibrarySyncToTeammates(player.userID); if (!_pluginConfig.BlueprintShareFromTechTree) return; TryShareBlueprintWithTeammates(itemDefinition, player); TryShareBlueprintWithClanmates(itemDefinition, player); } private void OnBlueprintLearned(BasePlayer player, ItemDefinition itemDefinition) { HandleTechTreeOrBlueprintLearnedHook(player, itemDefinition, nameof(OnBlueprintLearned)); } private void OnTechTreeNodeUnlocked(Workbench workbench, ItemDefinition itemDefinition, BasePlayer player) { if (!_pluginConfig.BlueprintShareFromTechTree || workbench == null || itemDefinition == null || player == null) return; HandleTechTreeOrBlueprintLearnedHook(player, itemDefinition, nameof(OnTechTreeNodeUnlocked)); } // Rust hook signature used by most builds (NodeInstance). private void OnTechTreeNodeUnlocked(Workbench workbench, TechTreeData.NodeInstance node, BasePlayer player) { if (!_pluginConfig.BlueprintShareFromTechTree || workbench == null || node == null || player == null) return; if (node.itemDef == null) return; HandleTechTreeOrBlueprintLearnedHook(player, node.itemDef, nameof(OnTechTreeNodeUnlocked)); } // Cancellable hook called during the tech tree unlock RPC, before the unlock is applied. // We schedule a sync for next tick so the player's unlock list is updated first. private object OnTechTreeNodeUnlock(Workbench workbench, TechTreeData.NodeInstance node, BasePlayer player) { if (!_pluginConfig.BlueprintShareFromTechTree || workbench == null || node == null || player == null) return null; if (!BlueprintSharingHooksEnabled()) return null; if (node.itemDef == null) return null; ulong sharerId = player.userID; var def = node.itemDef; NextTick(() => { var online = BasePlayer.FindByID(sharerId); if (online == null) return; // Run the same logic path we use for other blueprint-learn events. HandleTechTreeOrBlueprintLearnedHook(online, def, nameof(OnTechTreeNodeUnlock)); }); return null; } #endregion Team blueprint sharing #region Clan blueprint sharing (Clans Reborn) // Clans (k1lly0u) fires hooks with ulong userID, so we support both ulong and string variants. private void OnClanMemberJoined(ulong userID, List memberUserIDs) { if (!ClanBlueprintSharingEnabled()) return; var existing = new List(); if (memberUserIDs != null) { foreach (var idStr in memberUserIDs) { if (ulong.TryParse(idStr, out var id)) existing.Add(id); } } HandleClanMemberJoined(userID, existing); } private void OnClanMemberJoined(ulong userID, string tag) { if (!ClanBlueprintSharingEnabled()) return; var members = FetchClanMembersByTag(tag); HandleClanMemberJoined(userID, members.Where(id => id != userID)); _clanMembersByTag[tag] = members; } private void OnClanMemberJoined(string userID, List memberUserIDs) { if (!ClanBlueprintSharingEnabled()) return; if (!ulong.TryParse(userID, out var joiningId)) return; var existing = new List(); if (memberUserIDs != null) { foreach (var idStr in memberUserIDs) { if (ulong.TryParse(idStr, out var id)) existing.Add(id); } } HandleClanMemberJoined(joiningId, existing); } private void OnClanMemberJoined(string userID, string tag) { if (!ClanBlueprintSharingEnabled()) return; if (!ulong.TryParse(userID, out var joiningId)) return; var members = FetchClanMembersByTag(tag); HandleClanMemberJoined(joiningId, members.Where(id => id != joiningId)); _clanMembersByTag[tag] = members; } private void OnClanMemberGone(ulong userID, List memberUserIDs) { if (!ClanBlueprintSharingEnabled()) return; HandleClanMemberGone(userID, memberUserIDs); } private void OnClanMemberGone(ulong userID, List memberUserIDs) { if (!ClanBlueprintSharingEnabled()) return; var remaining = new List(); if (memberUserIDs != null) { foreach (var idStr in memberUserIDs) { if (ulong.TryParse(idStr, out var id)) remaining.Add(id); } } HandleClanMemberGone(userID, remaining); } private void OnClanMemberGone(ulong userID, string tag) { if (!ClanBlueprintSharingEnabled()) return; var remaining = FetchClanMembersByTag(tag); HandleClanMemberGone(userID, remaining.Where(id => id != userID)); _clanMembersByTag[tag] = remaining; } private void OnClanMemberGone(string userID, List memberUserIDs) { if (!ClanBlueprintSharingEnabled()) return; if (!ulong.TryParse(userID, out var leavingId)) return; var remaining = new List(); if (memberUserIDs != null) { foreach (var idStr in memberUserIDs) { if (ulong.TryParse(idStr, out var id)) remaining.Add(id); } } HandleClanMemberGone(leavingId, remaining); } private void OnClanMemberGone(string userID, string tag) { if (!ClanBlueprintSharingEnabled()) return; if (!ulong.TryParse(userID, out var leavingId)) return; var remaining = FetchClanMembersByTag(tag); HandleClanMemberGone(leavingId, remaining.Where(id => id != leavingId)); _clanMembersByTag[tag] = remaining; } private void OnClanDisbanded(List memberUserIDs) { if (!ClanBlueprintSharingEnabled()) return; if (memberUserIDs == null || memberUserIDs.Count == 0) return; var ids = new List(); foreach (var idStr in memberUserIDs) { if (ulong.TryParse(idStr, out var id)) ids.Add(id); } foreach (var memberId in ids) { HandleClanMemberGone(memberId, ids.Where(x => x != memberId)); } } private void OnClanDisbanded(string tag) { if (!ClanBlueprintSharingEnabled()) return; if (string.IsNullOrEmpty(tag)) return; OnClanDestroy(tag); } private void OnClanDisbanded(string tag, List memberUserIDs) { OnClanDisbanded(memberUserIDs); } private void OnClanCreate(string tag) { if (!ClanBlueprintSharingEnabled()) return; if (string.IsNullOrEmpty(tag)) return; _clanMembersByTag[tag] = FetchClanMembersByTag(tag); } private void OnClanUpdate(string tag) { if (!ClanBlueprintSharingEnabled()) return; if (string.IsNullOrEmpty(tag)) return; var newMembers = FetchClanMembersByTag(tag); _clanMembersByTag.TryGetValue(tag, out var oldMembers); oldMembers ??= new HashSet(); var added = newMembers.Except(oldMembers).ToList(); var removed = oldMembers.Except(newMembers).ToList(); if (_pluginConfig.DebugMode) Puts($"TeamShare: OnClanUpdate tag={tag} added={added.Count} removed={removed.Count}"); foreach (var joinerId in added) { HandleClanMemberJoined(joinerId, oldMembers.Where(id => id != joinerId)); } foreach (var leaverId in removed) { HandleClanMemberGone(leaverId, newMembers.Where(id => id != leaverId)); } _clanMembersByTag[tag] = newMembers; } private void OnClanDestroy(string tag) { if (!ClanBlueprintSharingEnabled()) return; if (string.IsNullOrEmpty(tag)) return; if (!_clanMembersByTag.TryGetValue(tag, out var members) || members == null || members.Count == 0) { _clanMembersByTag.Remove(tag); return; } foreach (var memberId in members) { HandleClanMemberGone(memberId, members.Where(id => id != memberId)); } _clanMembersByTag.Remove(tag); } #endregion Clan blueprint sharing #region Mode 2 entity cache and backward auth private EntityCache GetOrCreateEntityCache(ulong ownerId) { if (!_playerEntities.TryGetValue(ownerId, out EntityCache cache)) { cache = new EntityCache(); _playerEntities.Add(ownerId, cache); } return cache; } private void AddEntityToCache(ulong ownerId, CodeLock codeLock) { if (codeLock == null || codeLock.IsDestroyed) return; GetOrCreateEntityCache(ownerId).CodeLocks.Add(codeLock); if (_pluginConfig.DebugMode) Puts($"Mode 2: CodeLock added to cache for owner {ownerId}."); } private void AddEntityToCache(ulong ownerId, BuildingPrivlidge cupboard) { if (cupboard == null || cupboard.IsDestroyed) return; GetOrCreateEntityCache(ownerId).BuildingPrivlidges.Add(cupboard); if (_pluginConfig.DebugMode) Puts($"Mode 2: Cupboard added to cache for owner {ownerId}."); } private void AddEntityToCache(ulong ownerId, AutoTurret turret) { if (turret == null || turret.IsDestroyed) return; GetOrCreateEntityCache(ownerId).AutoTurrets.Add(turret); if (_pluginConfig.DebugMode) Puts($"Mode 2: Turret added to cache for owner {ownerId}."); } private void RemoveEntityFromCache(CodeLock codeLock) { if (codeLock == null) return; ulong ownerId = codeLock.OwnerID != 0 ? codeLock.OwnerID : codeLock.GetParentEntity()?.OwnerID ?? 0; if (ownerId == 0) return; if (_playerEntities.TryGetValue(ownerId, out EntityCache cache)) { cache.CodeLocks.Remove(codeLock); if (_pluginConfig.DebugMode) Puts($"Mode 2: CodeLock removed from cache for owner {ownerId}."); } } private void RemoveEntityFromCache(BuildingPrivlidge cupboard) { if (cupboard == null) return; if (cupboard.OwnerID == 0) return; if (_playerEntities.TryGetValue(cupboard.OwnerID, out EntityCache cache)) { cache.BuildingPrivlidges.Remove(cupboard); if (_pluginConfig.DebugMode) Puts($"Mode 2: Cupboard removed from cache for owner {cupboard.OwnerID}."); } } private void RemoveEntityFromCache(AutoTurret turret) { if (turret == null) return; if (turret.OwnerID == 0) return; if (_playerEntities.TryGetValue(turret.OwnerID, out EntityCache cache)) { cache.AutoTurrets.Remove(turret); if (_pluginConfig.DebugMode) Puts($"Mode 2: Turret removed from cache for owner {turret.OwnerID}."); } } private List GetTeamMembers(ulong playerId) { var team = RelationshipManager.ServerInstance?.FindPlayersTeam(playerId); if (team == null) return new List { playerId }; return new List(team.members); } private void RefreshOwnerEntityAuth(ulong ownerId, ulong newTeammateId = 0) { if (!_playerEntities.TryGetValue(ownerId, out EntityCache cache)) return; List teamMembers = GetTeamMembers(ownerId); int codelocksUpdated = 0, turretsUpdated = 0, cupboardsUpdated = 0; foreach (var turret in cache.AutoTurrets.ToList()) { if (turret == null || turret.IsDestroyed) continue; turret.authorizedPlayers.Clear(); foreach (ulong id in teamMembers) turret.authorizedPlayers.Add(id); turret.SendNetworkUpdate(); turretsUpdated++; } foreach (var cupboard in cache.BuildingPrivlidges.ToList()) { if (cupboard == null || cupboard.IsDestroyed) continue; cupboard.authorizedPlayers.Clear(); foreach (ulong id in teamMembers) cupboard.authorizedPlayers.Add(id); cupboard.SendNetworkUpdate(); cupboardsUpdated++; } foreach (var codeLock in cache.CodeLocks.ToList()) { if (codeLock == null || codeLock.IsDestroyed) continue; codeLock.whitelistPlayers.Clear(); foreach (ulong id in teamMembers) codeLock.whitelistPlayers.Add(id); codeLock.guestPlayers.Clear(); foreach (ulong id in teamMembers) { if (id != ownerId) codeLock.guestPlayers.Add(id); } codeLock.SendNetworkUpdate(); codelocksUpdated++; } if (newTeammateId != 0 && ownerId != newTeammateId && (codelocksUpdated + turretsUpdated + cupboardsUpdated) > 0) { BasePlayer owner = BasePlayer.FindByID(ownerId); if (owner != null && owner.IsConnected && !GetPlayerData(ownerId).enabledQuiet) { string newTeammateName = covalence.Players.FindPlayerById(newTeammateId.ToString())?.Name ?? "Unknown"; string msg = Lang("AuthedBackward", owner.UserIDString); SendChatMessage(owner, msg, newTeammateName, codelocksUpdated, turretsUpdated, cupboardsUpdated); } } if (_pluginConfig.DebugMode) Puts($"Mode 2: Refreshed auth for owner {ownerId} ({teamMembers.Count} members)."); } private void UpdateTeamAuthList(IList affectedMemberIds, ulong newTeammateId = 0) { if (affectedMemberIds == null || affectedMemberIds.Count == 0) return; foreach (ulong memberId in affectedMemberIds) { if (_pluginConfig.UsePermissions && !permission.UserHasPermission(memberId.ToString(), PERMISSION_USE)) continue; RefreshOwnerEntityAuth(memberId, newTeammateId); } } void OnEntityKill(AutoTurret turret) { if (_pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH) RemoveEntityFromCache(turret); } void OnEntityKill(BuildingPrivlidge cupboard) { if (_pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH) RemoveEntityFromCache(cupboard); } void OnEntityKill(CodeLock codeLock) { if (_pluginConfig.OperatingMode == OPERATING_MODE_BACKWARD_AUTH) RemoveEntityFromCache(codeLock); } void OnTeamAcceptInvite(RelationshipManager.PlayerTeam playerTeam, BasePlayer player) { HandleBlueprintTeamInviteAccepted(playerTeam, player); if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; if (playerTeam == null || player == null) return; ulong newTeammateId = player.userID; NextTick(() => { if (playerTeam.members.Contains(newTeammateId)) { UpdateTeamAuthList(playerTeam.members, newTeammateId); } }); } void OnPlayerDisconnected(BasePlayer player, string reason) { if (player != null) CancelBlueprintLibraryResyncTimer(player.userID); } void OnTeamLeave(RelationshipManager.PlayerTeam playerTeam, BasePlayer player) { if (player != null) { CancelBlueprintLibraryResyncTimer(player.userID); HandleBlueprintPlayerLeftTeam(player.userID, playerTeam?.members); } if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; if (playerTeam == null || player == null) return; NextTick(() => { if (!playerTeam.members.Contains(player.userID)) { var affected = new List(playerTeam.members) { player.userID }; UpdateTeamAuthList(affected); } }); } void OnTeamKick(RelationshipManager.PlayerTeam playerTeam, BasePlayer leader, ulong target) { CancelBlueprintLibraryResyncTimer(target); HandleBlueprintPlayerLeftTeam(target, playerTeam?.members); if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; if (playerTeam == null) return; NextTick(() => { if (!playerTeam.members.Contains(target)) { var affected = new List(playerTeam.members) { target }; UpdateTeamAuthList(affected); } }); } void OnTeamDisbanded(RelationshipManager.PlayerTeam playerTeam) { if (playerTeam?.members != null) { foreach (ulong memberId in playerTeam.members) { CancelBlueprintLibraryResyncTimer(memberId); HandleBlueprintPlayerLeftTeam(memberId); } } if (_pluginConfig.OperatingMode != OPERATING_MODE_BACKWARD_AUTH) return; if (playerTeam == null) return; UpdateTeamAuthList(playerTeam.members); } #endregion Mode 2 entity cache and backward auth #endregion #region UI Methods [ChatCommand("teamui")] private void OpenTeamShareUI(BasePlayer player, string command, string[] args) { if (!IsPermitted(player)) { SendChatMessage(player, Lang("NoPermission", player.UserIDString)); return; } CreateUI(player); } private void CreateUI(BasePlayer player, string parentPanel = "Overlay") { DestroyUI(player); CuiElementContainer container = new CuiElementContainer(); string panelName = CuiHelper.GetGuid(); playerUIs[player.userID] = panelName; string anchorMin = !_pluginConfig.UsingWelcomeController ? "0.2 0.2" : "0 0"; string anchorMax = !_pluginConfig.UsingWelcomeController ? "0.8 0.8" : "1 1"; int fontSize = !_pluginConfig.UsingWelcomeController ? 16 : 20; int buttonFontSize = !_pluginConfig.UsingWelcomeController ? 14 : 18; string fieldBackgroundColor = !_pluginConfig.UsingWelcomeController ? "0 0 0 0.7" : uiSettings.UIBackgroundColor; CreatePanel(ref container, anchorMin, anchorMax, uiSettings.UIBackgroundColor, parentPanel, panelName, isMainPanel: true, blur: true); #region Top Panel string topPanel = CreatePanel(ref container, _pluginConfig.UsingWelcomeController ? "0.1 0.65" : "0.1 0.652", _pluginConfig.UsingWelcomeController ? "0.9 0.9" : "0.9 0.9", uiSettings.UIBackgroundColor, panelName, "TopPanel"); CreateLabel(ref container, "0 0.75", "1 1", "1 1 1 1", "0 0 0 0", "Team Auth Options", fontSize, TextAnchor.MiddleCenter, topPanel); if (!_pluginConfig.UsingWelcomeController) { CreateButton(ref container, "0.92 0.92", "0.98 0.98", uiSettings.UIDangerButtonColor, "1 1 1 1", "×", fontSize, "teamshare_close_ui", panelName, TextAnchor.MiddleCenter); } var sharingToggles = new List(); if (HasFeaturePermission(player, PERMISSION_CODELOCK)) sharingToggles.Add(new ToggleOption { Label = "Codelock Sharing", Command = "toggle_codelock", IsOn = GetPlayerData(player.userID).enabledCodelock, Permission = PERMISSION_CODELOCK }); if (HasFeaturePermission(player, PERMISSION_CUPBOARD)) sharingToggles.Add(new ToggleOption { Label = "Cupboard Sharing", Command = "toggle_cupboard", IsOn = GetPlayerData(player.userID).enabledCupboard, Permission = PERMISSION_CUPBOARD }); if (HasFeaturePermission(player, PERMISSION_TURRET)) sharingToggles.Add(new ToggleOption { Label = "Turret Sharing", Command = "toggle_turret", IsOn = GetPlayerData(player.userID).enabledTurret, Permission = PERMISSION_TURRET }); var accessControlToggles = new List(); if (HasFeaturePermission(player, PERMISSION_GUEST)) accessControlToggles.Add(new ToggleOption { Label = "Enable Guest Code", Command = "toggle_guestcode", IsOn = GetPlayerData(player.userID).enabledGuestCode, Permission = PERMISSION_GUEST }); accessControlToggles.Add(new ToggleOption { Label = "Quiet Mode", Command = "toggle_quiet", IsOn = GetPlayerData(player.userID).enabledQuiet, Permission = null }); float startY = _pluginConfig.UsingWelcomeController ? 0.5f : 0.5f; float rowHeight = _pluginConfig.UsingWelcomeController ? 0.2f : 0.23f; float rowSpacing = _pluginConfig.UsingWelcomeController ? 0.25f : 0.26f; foreach (var toggle in sharingToggles) { int index = sharingToggles.IndexOf(toggle); float yMin = startY - index * rowSpacing; float yMax = yMin + rowHeight; string toggleName = toggle.Command.Substring(7); string toggleGroup = CreatePanel(ref container, $"0.05 {yMin}", $"0.95 {yMax}", fieldBackgroundColor, topPanel, $"ToggleGroup_{toggleName}"); CreateLabel(ref container, "0.05 0", "0.6 1", "1 1 1 1", "0 0 0 0", toggle.Label, fontSize, TextAnchor.MiddleLeft, toggleGroup); CreateToggleSwitch(ref container, "0.75 0.2", "0.95 0.8", toggle.IsOn, $"teamshare_{toggle.Command}", toggleGroup, $"ToggleButton_{toggleName}", buttonFontSize); } #endregion #region Middle Panel string middlePanel = CreatePanel(ref container, _pluginConfig.UsingWelcomeController ? "0.1 0.4" : "0.1 0.4", _pluginConfig.UsingWelcomeController ? "0.9 0.648" : "0.9 0.65", uiSettings.UIBackgroundColor, panelName, "MiddlePanel"); CreateLabel(ref container, "0 0.7", "1 1", "1 1 1 1", "0 0 0 0", "Access Control", fontSize, TextAnchor.MiddleCenter, middlePanel); foreach (var toggle in accessControlToggles) { int index = accessControlToggles.IndexOf(toggle); float yMin = startY - index * rowSpacing; float yMax = yMin + rowHeight; string toggleName = toggle.Command.Substring(7); string toggleGroup = CreatePanel(ref container, $"0.05 {yMin}", $"0.95 {yMax}", fieldBackgroundColor, middlePanel, $"ToggleGroup_{toggleName}"); CreateLabel(ref container, "0.05 0", "0.6 1", "1 1 1 1", "0 0 0 0", toggle.Label, fontSize, TextAnchor.MiddleLeft, toggleGroup); CreateToggleSwitch(ref container, "0.75 0.2", "0.95 0.8", toggle.IsOn, $"teamshare_{toggle.Command}", toggleGroup, $"ToggleButton_{toggleName}", buttonFontSize); } #endregion #region Code Panels string leftPanel = CreatePanel(ref container, "0.1 0.1", _pluginConfig.UsingWelcomeController ? "0.4 0.35" : "0.4 0.35", uiSettings.UIBackgroundColor, panelName, "LeftPanel"); CreateLabel(ref container, "0.05 0.75", "0.95 0.95", "1 1 1 1", "0 0 0 0", "Main Code:", fontSize, TextAnchor.MiddleCenter, leftPanel); string mainCodeDisplay = GetPlayerCode(player.userID) ?? ""; if (!playerCodeRevealed.ContainsKey(player.userID)) { playerCodeRevealed[player.userID] = false; } bool isMainRevealed = playerCodeRevealed[player.userID]; CreatePanel(ref container, "0.1 0.5", "0.9 0.7", fieldBackgroundColor, leftPanel, "InputFieldBackground_MainCode"); string inputFieldText = ""; if (playerTempCodes.ContainsKey(player.userID)) { inputFieldText = playerTempCodes[player.userID]; } else if (isMainRevealed && !string.IsNullOrEmpty(mainCodeDisplay)) { inputFieldText = mainCodeDisplay; } container.Add(new CuiElement { Name = "MainCodeInputField", Parent = "InputFieldBackground_MainCode", Components = { new CuiInputFieldComponent { Command = "teamshare_set_temp_code", FontSize = fontSize, Align = TextAnchor.MiddleCenter, Text = inputFieldText, Color = "1 1 1 1", NeedsKeyboard = true }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); if (!string.IsNullOrEmpty(mainCodeDisplay) && !isMainRevealed) { CreateButton(ref container, "0.1 0.5", "0.9 0.7", "0 0 0 0", "1 1 1 1", "****", fontSize, "teamshare_toggle_reveal_main", leftPanel, TextAnchor.MiddleCenter, "MainCodeDisplay"); } CreateButton(ref container, "0.1 0.15", "0.9 0.3", uiSettings.UIPrimaryColor, "1 1 1 1", "Set Code", buttonFontSize, "teamshare_confirm_code", leftPanel); if (HasFeaturePermission(player, PERMISSION_GUEST)) { string rightPanel = CreatePanel(ref container, "0.6 0.1", _pluginConfig.UsingWelcomeController ? "0.9 0.35" : "0.9 0.35", uiSettings.UIBackgroundColor, panelName, "RightPanel"); CreateLabel(ref container, "0.05 0.75", "0.95 0.95", "1 1 1 1", "0 0 0 0", "Guest Code:", fontSize, TextAnchor.MiddleCenter, rightPanel); string guestCodeDisplay = GetPlayerGuestCode(player.userID) ?? ""; if (!playerGuestCodeRevealed.ContainsKey(player.userID)) { playerGuestCodeRevealed[player.userID] = false; } bool isGuestRevealed = playerGuestCodeRevealed[player.userID]; CreatePanel(ref container, "0.1 0.5", "0.9 0.7", fieldBackgroundColor, rightPanel, "InputFieldBackground_GuestCode"); string guestInputFieldText = ""; if (playerTempGuestCodes.ContainsKey(player.userID)) { guestInputFieldText = playerTempGuestCodes[player.userID]; } else if (isGuestRevealed && !string.IsNullOrEmpty(guestCodeDisplay)) { guestInputFieldText = guestCodeDisplay; } container.Add(new CuiElement { Name = "GuestCodeInputField", Parent = "InputFieldBackground_GuestCode", Components = { new CuiInputFieldComponent { Command = "teamshare_set_temp_guest_code", FontSize = fontSize, Align = TextAnchor.MiddleCenter, Text = guestInputFieldText, Color = "1 1 1 1", NeedsKeyboard = true }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); if (!string.IsNullOrEmpty(guestCodeDisplay) && !isGuestRevealed) { CreateButton(ref container, "0.1 0.5", "0.9 0.7", "0 0 0 0", "1 1 1 1", "****", fontSize, "teamshare_toggle_reveal_guest", rightPanel, TextAnchor.MiddleCenter, "GuestCodeDisplay"); } CreateButton(ref container, "0.1 0.15", "0.9 0.3", uiSettings.UIPrimaryColor, "1 1 1 1", "Set Guest Code", buttonFontSize, "teamshare_confirm_guest_code", rightPanel); } #endregion CuiHelper.AddUi(player, container); if (_pluginConfig.DebugMode) { Puts($"UI created for player {player.displayName}."); } } #endregion #region UI Management private void DestroyUI(BasePlayer player) { if (playerUIs.TryGetValue(player.userID, out string panelName)) { CuiHelper.DestroyUi(player, panelName); playerUIs.Remove(player.userID); playerTempCodes.Remove(player.userID); playerTempGuestCodes.Remove(player.userID); playerCodeRevealed.Remove(player.userID); playerGuestCodeRevealed.Remove(player.userID); if (codeChangedTimers.TryGetValue(player.userID, out var codeTimer)) { codeTimer.Destroy(); codeChangedTimers.Remove(player.userID); } if (guestCodeChangedTimers.TryGetValue(player.userID, out var guestCodeTimer)) { guestCodeTimer.Destroy(); guestCodeChangedTimers.Remove(player.userID); } var animationKeysToRemove = new List(); string playerPrefix = $"{player.userID}_"; foreach (var kvp in animationTimers) { if (kvp.Key.StartsWith(playerPrefix)) { kvp.Value.Destroy(); animationKeysToRemove.Add(kvp.Key); } } foreach (var key in animationKeysToRemove) { animationTimers.Remove(key); } if (WelcomeController != null) { WelcomeController.Call("DestroyUI", player); } if (_pluginConfig.DebugMode) { Puts($"UI destroyed for player {player.displayName}."); } } } private void UpdateToggleButton(BasePlayer player, string toggleName, bool isOn) { string toggleButtonName = $"ToggleButton_{toggleName}"; string toggleGroupName = $"ToggleGroup_{toggleName}"; string buttonCommand = $"teamshare_toggle_{toggleName}"; int buttonFontSize = !_pluginConfig.UsingWelcomeController ? 14 : 18; if (_pluginConfig.UISettings.AnimatedButtons) { AnimateToggleSwitch(player, toggleGroupName, toggleButtonName, buttonCommand, !isOn, isOn, buttonFontSize); } else { CuiHelper.DestroyUi(player, $"{toggleButtonName}_Container"); var container = new CuiElementContainer(); CreateToggleSwitch(ref container, "0.75 0.2", "0.95 0.8", isOn, buttonCommand, toggleGroupName, toggleButtonName, buttonFontSize); CuiHelper.AddUi(player, container); } if (_pluginConfig.DebugMode) { Puts($"Toggle '{toggleName}' updated to {(isOn ? "On" : "Off")} for player {player.displayName}."); } } private void AnimateToggleSwitch(BasePlayer player, string parent, string buttonName, string buttonCommand, bool fromState, bool toState, int fontSize) { string animationKey = $"{player.userID}_{buttonName}"; if (animationTimers.TryGetValue(animationKey, out var existingTimer)) { existingTimer.Destroy(); animationTimers.Remove(animationKey); CuiHelper.DestroyUi(player, $"{buttonName}_Container"); var instantContainer = new CuiElementContainer(); CreateToggleSwitch(ref instantContainer, "0.75 0.2", "0.95 0.8", toState, buttonCommand, parent, buttonName, fontSize); CuiHelper.AddUi(player, instantContainer); return; } int steps = 24; float interval = 0.25f / steps; Timer animTimer = null; void RunStep(int step) { if (step > steps) { animTimer?.Destroy(); animationTimers.Remove(animationKey); var finalContainer = new CuiElementContainer(); CreateToggleSwitch(ref finalContainer, "0.75 0.2", "0.95 0.8", toState, buttonCommand, parent, buttonName, fontSize); CuiHelper.DestroyUi(player, $"{buttonName}_Container"); CuiHelper.AddUi(player, finalContainer); return; } float progress = (float)step / steps; float easedProgress = progress * progress * (3f - 2f * progress); var container = new CuiElementContainer(); string toggleContainer = $"{buttonName}_Container"; float onPanelMaxStart = fromState ? 1f : 0f; float onPanelMaxEnd = toState ? 1f : 0f; float onPanelMax = onPanelMaxStart + (onPanelMaxEnd - onPanelMaxStart) * easedProgress; float offPanelMinStart = fromState ? 1f : 0f; float offPanelMinEnd = toState ? 1f : 0f; float offPanelMin = offPanelMinStart + (offPanelMinEnd - offPanelMinStart) * easedProgress; float offPanelMax = 1f; if (onPanelMax > 0.001f) { CreatePanel(ref container, "0 0", $"{onPanelMax} 1", uiSettings.UIToggleOnColor, toggleContainer, $"{buttonName}_OnPanel"); CreateLabel(ref container, "0 0", "1 1", "1 1 1 1", "0 0 0 0", "ON", fontSize, TextAnchor.MiddleCenter, $"{buttonName}_OnPanel"); } if (offPanelMin < offPanelMax - 0.001f) { CreatePanel(ref container, $"{offPanelMin} 0", $"{offPanelMax} 1", uiSettings.UIToggleOffColor, toggleContainer, $"{buttonName}_OffPanel"); CreateLabel(ref container, "0 0", "1 1", "1 1 1 1", "0 0 0 0", "OFF", fontSize, TextAnchor.MiddleCenter, $"{buttonName}_OffPanel"); } CuiHelper.AddUi(player, container); if (step < steps) { animTimer = timer.Once(interval, () => RunStep(step + 1)); animationTimers[animationKey] = animTimer; } else { RunStep(step + 1); } } animTimer = timer.Once(interval, () => RunStep(1)); animationTimers[animationKey] = animTimer; } #endregion #region Command Handlers [ConsoleCommand("teamshare_close_ui")] private void CloseUI(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; DestroyUI(player); } [ConsoleCommand("teamshare_toggle_codelock")] private void ToggleCodelock(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; PlayerData pData = GetPlayerData(player.userID); pData.enabledCodelock = !pData.enabledCodelock; SavePlayerData(); UpdateToggleButton(player, "codelock", pData.enabledCodelock); } [ConsoleCommand("teamshare_toggle_cupboard")] private void ToggleCupboard(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; PlayerData pData = GetPlayerData(player.userID); pData.enabledCupboard = !pData.enabledCupboard; SavePlayerData(); UpdateToggleButton(player, "cupboard", pData.enabledCupboard); } [ConsoleCommand("teamshare_toggle_turret")] private void ToggleTurret(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; PlayerData pData = GetPlayerData(player.userID); pData.enabledTurret = !pData.enabledTurret; SavePlayerData(); UpdateToggleButton(player, "turret", pData.enabledTurret); } [ConsoleCommand("teamshare_toggle_quiet")] private void ToggleQuiet(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; PlayerData pData = GetPlayerData(player.userID); pData.enabledQuiet = !pData.enabledQuiet; SavePlayerData(); UpdateToggleButton(player, "quiet", pData.enabledQuiet); } [ConsoleCommand("teamshare_toggle_guestcode")] private void ToggleGuestCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; if (!HasFeaturePermission(player, PERMISSION_GUEST)) return; PlayerData pData = GetPlayerData(player.userID); pData.enabledGuestCode = !pData.enabledGuestCode; SavePlayerData(); UpdateToggleButton(player, "guestcode", pData.enabledGuestCode); } [ConsoleCommand("teamshare_set_temp_code")] private void SetTempCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; string code = arg.GetString(0, ""); playerTempCodes[player.userID] = code; if (_pluginConfig.DebugMode) { Puts($"Temporary main code set to '{code}' for player {player.displayName}."); } } [ConsoleCommand("teamshare_set_temp_guest_code")] private void SetTempGuestCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; if (!HasFeaturePermission(player, PERMISSION_GUEST)) return; string code = arg.GetString(0, ""); playerTempGuestCodes[player.userID] = code; if (_pluginConfig.DebugMode) { Puts($"Temporary guest code set to '{code}' for player {player.displayName}."); } } [ConsoleCommand("teamshare_toggle_reveal_main")] private void ToggleRevealMainCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; if (!playerCodeRevealed.ContainsKey(player.userID) || !playerCodeRevealed[player.userID]) { playerCodeRevealed[player.userID] = true; UpdateCodeDisplay(player, isGuestCode: false); } } [ConsoleCommand("teamshare_toggle_reveal_guest")] private void ToggleRevealGuestCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; if (!HasFeaturePermission(player, PERMISSION_GUEST)) return; if (!playerGuestCodeRevealed.ContainsKey(player.userID) || !playerGuestCodeRevealed[player.userID]) { playerGuestCodeRevealed[player.userID] = true; UpdateCodeDisplay(player, isGuestCode: true); } } private void UpdateCodeDisplay(BasePlayer player, bool isGuestCode = false) { var container = new CuiElementContainer(); int fontSize = !_pluginConfig.UsingWelcomeController ? 16 : 20; if (isGuestCode) { string guestCodeDisplay = GetPlayerGuestCode(player.userID) ?? ""; bool isRevealed = playerGuestCodeRevealed.ContainsKey(player.userID) && playerGuestCodeRevealed[player.userID]; if (!string.IsNullOrEmpty(guestCodeDisplay) && !isRevealed) { CreateButton(ref container, "0.1 0.5", "0.9 0.7", "0 0 0 0", "1 1 1 1", "****", fontSize, "teamshare_toggle_reveal_guest", "RightPanel", TextAnchor.MiddleCenter, "GuestCodeDisplay"); } if (isRevealed && !string.IsNullOrEmpty(guestCodeDisplay)) { CuiHelper.DestroyUi(player, "GuestCodeInputField"); container.Add(new CuiElement { Name = "GuestCodeInputField", Parent = "InputFieldBackground_GuestCode", Components = { new CuiInputFieldComponent { Command = "teamshare_set_temp_guest_code", FontSize = fontSize, Align = TextAnchor.MiddleCenter, Text = playerTempGuestCodes.ContainsKey(player.userID) ? playerTempGuestCodes[player.userID] : guestCodeDisplay, Color = "1 1 1 1", NeedsKeyboard = true }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } } else { string mainCodeDisplay = GetPlayerCode(player.userID) ?? ""; bool isRevealed = playerCodeRevealed.ContainsKey(player.userID) && playerCodeRevealed[player.userID]; if (!string.IsNullOrEmpty(mainCodeDisplay) && !isRevealed) { CreateButton(ref container, "0.1 0.5", "0.9 0.7", "0 0 0 0", "1 1 1 1", "****", fontSize, "teamshare_toggle_reveal_main", "LeftPanel", TextAnchor.MiddleCenter, "MainCodeDisplay"); } if (isRevealed && !string.IsNullOrEmpty(mainCodeDisplay)) { CuiHelper.DestroyUi(player, "MainCodeInputField"); container.Add(new CuiElement { Name = "MainCodeInputField", Parent = "InputFieldBackground_MainCode", Components = { new CuiInputFieldComponent { Command = "teamshare_set_temp_code", FontSize = fontSize, Align = TextAnchor.MiddleCenter, Text = playerTempCodes.ContainsKey(player.userID) ? playerTempCodes[player.userID] : mainCodeDisplay, Color = "1 1 1 1", NeedsKeyboard = true }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } } CuiHelper.DestroyUi(player, isGuestCode ? "GuestCodeDisplay" : "MainCodeDisplay"); CuiHelper.AddUi(player, container); } [ConsoleCommand("teamshare_confirm_code")] private void ConfirmCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; string code = ""; if (playerTempCodes.TryGetValue(player.userID, out string tempCode) && !string.IsNullOrEmpty(tempCode) && tempCode != "****") { code = tempCode; } else { string currentCode = GetPlayerCode(player.userID); if (!string.IsNullOrEmpty(currentCode)) { code = currentCode; } else { SendChatMessage(player, Lang("NoCodeSet", player.UserIDString)); return; } } code = new string(code.Where(char.IsDigit).ToArray()); if (string.IsNullOrEmpty(code)) { SendChatMessage(player, Lang("InvalidCode", player.UserIDString)); ShowCodeChangedMessage(player, "LeftPanel", "CodeChangedLabel_MainCode", isGuestCode: false, isValid: false); return; } code = code.PadLeft(4, '0'); if (code.Length > 4) { code = code.Substring(0, 4); } // Validate 4-digit code (0000-9999) if (!IsValidCode(code)) { SendChatMessage(player, Lang("InvalidCode", player.UserIDString)); ShowCodeChangedMessage(player, "LeftPanel", "CodeChangedLabel_MainCode", isGuestCode: false, isValid: false); return; } SetPlayerCode(player.userID, code); SendChatMessage(player, string.Format(Lang("CodeSet", player.UserIDString), code)); ShowCodeChangedMessage(player, "LeftPanel", "CodeChangedLabel_MainCode", isGuestCode: false, isValid: true); playerTempCodes.Remove(player.userID); playerCodeRevealed[player.userID] = false; CreateUI(player); } [ConsoleCommand("teamshare_confirm_guest_code")] private void ConfirmGuestCode(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; if (!HasFeaturePermission(player, PERMISSION_GUEST)) return; string code = ""; if (playerTempGuestCodes.TryGetValue(player.userID, out string tempCode) && !string.IsNullOrEmpty(tempCode) && tempCode != "****") { code = tempCode; } else { string currentCode = GetPlayerGuestCode(player.userID); if (!string.IsNullOrEmpty(currentCode)) { code = currentCode; } else { SendChatMessage(player, Lang("NoGuestCodeSet", player.UserIDString)); return; } } code = new string(code.Where(char.IsDigit).ToArray()); if (string.IsNullOrEmpty(code)) { SendChatMessage(player, Lang("InvalidCode", player.UserIDString)); ShowCodeChangedMessage(player, "GuestCodeSection", "CodeChangedLabel_GuestCode", isGuestCode: true, isValid: false); return; } code = code.PadLeft(4, '0'); if (code.Length > 4) { code = code.Substring(0, 4); } // Validate 4-digit code (0000-9999) if (!IsValidCode(code)) { SendChatMessage(player, Lang("InvalidCode", player.UserIDString)); ShowCodeChangedMessage(player, "RightPanel", "CodeChangedLabel_GuestCode", isGuestCode: true, isValid: false); return; } SetPlayerGuestCode(player.userID, code); SendChatMessage(player, string.Format(Lang("GuestCodeSet", player.UserIDString), code)); ShowCodeChangedMessage(player, "RightPanel", "CodeChangedLabel_GuestCode", isGuestCode: true, isValid: true); playerTempGuestCodes.Remove(player.userID); playerGuestCodeRevealed[player.userID] = false; CreateUI(player); } #endregion #region Helper Methods private string ResolvePlayerDisplayName(ulong userId) { var p = BasePlayer.FindByID(userId); if (p != null && !string.IsNullOrEmpty(p.displayName)) return p.displayName; return covalence.Players.FindPlayerById(userId.ToString())?.Name ?? "Teammate"; } private void SendChatMessage(BasePlayer player, string message, params object[] args) { if (GetPlayerData(player.userID).enabledQuiet) { if (_pluginConfig.DebugMode) { Puts($"Quiet mode enabled. Message '{message}' not sent to player {player.displayName}."); } return; } string formattedMessage = args.Length > 0 ? string.Format(message, args) : message; player.ChatMessage(formattedMessage); } private string Lang(string key, string userId = null) => lang.GetMessage(key, this, userId); private string CreatePanel(ref CuiElementContainer container, string anchorMin, string anchorMax, string panelColor, string parent = "Overlay", string panelName = null, bool isMainPanel = false, bool blur = false) { CuiPanel 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 CreateButton(ref CuiElementContainer container, string anchorMin, string anchorMax, string buttonColor, string textColor, string buttonText, int fontSize, string buttonCommand, string parent = "Overlay", TextAnchor labelAnchor = TextAnchor.MiddleCenter, string buttonName = null) { container.Add(new CuiButton { Button = { Color = buttonColor, Command = $"{buttonCommand}", Close = "" }, RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }, Text = { Align = labelAnchor, Color = textColor, FontSize = fontSize, Text = buttonText, Font = "robotocondensed-bold.ttf" } }, parent, buttonName); } private void CreateToggleSwitch(ref CuiElementContainer container, string anchorMin, string anchorMax, bool isOn, string buttonCommand, string parent, string buttonName, int fontSize) { string toggleContainer = CreatePanel(ref container, anchorMin, anchorMax, "0 0 0 0", parent, $"{buttonName}_Container"); float onPanelMin = isOn ? 0f : 0f; float onPanelMax = isOn ? 1f : 0f; float offPanelMin = isOn ? 1f : 0f; float offPanelMax = isOn ? 1f : 1f; if (onPanelMax > onPanelMin + 0.001f) { CreatePanel(ref container, $"{onPanelMin} 0", $"{onPanelMax} 1", uiSettings.UIToggleOnColor, toggleContainer, $"{buttonName}_OnPanel"); CreateLabel(ref container, "0 0", "1 1", "1 1 1 1", "0 0 0 0", "ON", fontSize, TextAnchor.MiddleCenter, $"{buttonName}_OnPanel"); } if (offPanelMax > offPanelMin + 0.001f) { CreatePanel(ref container, $"{offPanelMin} 0", $"{offPanelMax} 1", uiSettings.UIToggleOffColor, toggleContainer, $"{buttonName}_OffPanel"); CreateLabel(ref container, "0 0", "1 1", "1 1 1 1", "0 0 0 0", "OFF", fontSize, TextAnchor.MiddleCenter, $"{buttonName}_OffPanel"); } CreateButton(ref container, "0 0", "1 1", "0 0 0 0", "0 0 0 0", "", 1, buttonCommand, toggleContainer, TextAnchor.MiddleCenter, buttonName); } private void ShowCodeChangedMessage(BasePlayer player, string parentPanel, string labelName, bool isGuestCode = false, bool isValid = true) { string messageText = isValid ? "Code Changed" : "Invalid Code"; int fontSize = 14; float fadeDuration = 1.0f; int steps = 10; float interval = fadeDuration / steps; float alphaIncrement = 1f / steps; float alphaDecrement = 1f / steps; float currentAlpha = 1f; Timer fadeOutTimer = null; fadeOutTimer = timer.Repeat(interval, steps, () => { try { if (player == null || !player.IsConnected) { if (fadeOutTimer != null) fadeOutTimer.Destroy(); return; } currentAlpha -= alphaDecrement; if (currentAlpha < 0f) currentAlpha = 0f; CuiHelper.DestroyUi(player, labelName); var container = new CuiElementContainer(); container.Add(CreateLabelElement(player, parentPanel, labelName, $"1 1 1 {currentAlpha}", fontSize, "")); CuiHelper.AddUi(player, container); if (currentAlpha <= 0) { if (fadeOutTimer != null) fadeOutTimer.Destroy(); float newAlpha = 0f; Timer fadeInTimer = null; fadeInTimer = timer.Repeat(interval, steps, () => { try { if (player == null || !player.IsConnected) { if (fadeInTimer != null) fadeInTimer.Destroy(); return; } newAlpha += alphaIncrement; if (newAlpha > 1f) newAlpha = 1f; CuiHelper.DestroyUi(player, labelName); var containerFadeIn = new CuiElementContainer(); containerFadeIn.Add(CreateLabelElement(player, parentPanel, labelName, $"1 1 1 {newAlpha}", fontSize, messageText)); CuiHelper.AddUi(player, containerFadeIn); if (newAlpha >= 1f) { if (fadeInTimer != null) fadeInTimer.Destroy(); timer.Once(7f, () => { try { if (player == null || !player.IsConnected) return; Timer fadeOutAfterTimer = null; currentAlpha = 1f; fadeOutAfterTimer = timer.Repeat(interval, steps, () => { try { if (player == null || !player.IsConnected) { if (fadeOutAfterTimer != null) fadeOutAfterTimer.Destroy(); return; } currentAlpha -= alphaDecrement; if (currentAlpha < 0f) currentAlpha = 0f; CuiHelper.DestroyUi(player, labelName); var containerFadeOut = new CuiElementContainer(); containerFadeOut.Add(CreateLabelElement(player, parentPanel, labelName, $"1 1 1 {currentAlpha}", fontSize, messageText)); CuiHelper.AddUi(player, containerFadeOut); if (currentAlpha <= 0) { if (fadeOutAfterTimer != null) fadeOutAfterTimer.Destroy(); } } catch (Exception ex) { PrintError($"Error in fade out after timer: {ex.Message}"); if (fadeOutAfterTimer != null) fadeOutAfterTimer.Destroy(); } }); } catch (Exception ex) { PrintError($"Error setting up fade out after timer: {ex.Message}"); } }); } } catch (Exception ex) { PrintError($"Error in fade in timer: {ex.Message}"); if (fadeInTimer != null) fadeInTimer.Destroy(); } }); } } catch (Exception ex) { PrintError($"Error in fade out timer: {ex.Message}"); if (fadeOutTimer != null) fadeOutTimer.Destroy(); } }); if (_pluginConfig.DebugMode) { Puts($"Displayed '{messageText}' message for player {player.displayName}."); } } private CuiElement CreateLabelElement(BasePlayer player, string parentPanel, string labelName, string color, int fontSize, string messageText) { var labelElement = new CuiElement { Name = labelName, Parent = parentPanel, Components = { new CuiRectTransformComponent { AnchorMin = "0.1 0.01", AnchorMax = "0.9 0.13" }, new CuiTextComponent { Text = messageText, FontSize = fontSize, Align = TextAnchor.MiddleCenter, Color = color, Font = "robotocondensed-bold.ttf" } } }; return labelElement; } #endregion } }