using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Facepunch; using Network; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Configuration; using Oxide.Core.Libraries.Covalence; using Oxide.Game.Rust.Cui; using Rust; using Rust.AI; using Rust.Registry; using UnityEngine; using QueryGrid = BaseEntity.Query; namespace Oxide.Plugins { [Info("BetterVanish", "Def", "1.7.1")] [Description("Customized & Optimized \"Vanish\"")] public class BetterVanish : RustPlugin { // Hooks: OnVanishDisappear(BasePlayer), OnVanishReappear(BasePlayer) // API: IsInvisible(BasePlayer), IsInvisible(IPlayer), DoVanishDisappear(BasePlayer), DoVanishReappear(BasePlayer) #region Constants private const string PermUse = nameof(BetterVanish) + ".allowed"; private const string PermUseOther = nameof(BetterVanish) + ".allowedother"; private const string PermUnvanish = nameof(BetterVanish) + ".unvanish"; private const string PermInvSpy = nameof(BetterVanish) + ".invspy"; private const string PermSkipLocks = nameof(BetterVanish) + ".skiplocks"; private const string CuiName = "VanishIcon"; private const string PlayerPrefab = "assets/prefabs/player/player.prefab"; private const string PlayerCorpsePrefab = "assets/prefabs/player/player_corpse.prefab"; private const string EffectDisappear = "assets/bundled/prefabs/fx/gestures/cameratakescreenshot.prefab"; private const string EffectAppear = "assets/bundled/prefabs/fx/gestures/drink_vomit.prefab"; private static readonly Effect EffectInstance = new Effect(); private static readonly DamageTypeList EmptyDmgList = new DamageTypeList(); private static readonly GameObjectRef EmptyObjRef = new GameObjectRef(); private static readonly object FalseObj = false; private static readonly object TrueObj = true; // private enum VanishEvent { Disappear, Reappear, Disconnect, Unload } // private static readonly List HooksLst = new List { nameof(OnEntityMarkHostile), nameof(OnEntityTakeDamage), nameof(OnPlayerDisconnected), nameof(CanUseLockedEntity), nameof(OnPlayerViolation), nameof(OnTeamInvite), nameof(OnPlayerColliderEnable) }; #endregion #region Fields private static BetterVanish _instance; private static CuiElementContainer _cui; private GameObjectRef _fallDmgEff = EmptyObjRef, _drownEff = EmptyObjRef; private bool _isShutdown; private bool _lastHooksState = true; #endregion #region Configuration private const int CfgRev = 3; private static Configuration _config; public class Configuration { [JsonProperty(PropertyName = "Show visual indicator (true/false)")] public bool ShowIndicator; [JsonProperty(PropertyName = "Visual indicator image address")] public string IndicatorAddress; [JsonProperty(PropertyName = "Visual indicator anchor min")] public string IndicatorAnchorMin; [JsonProperty(PropertyName = "Visual indicator anchor max")] public string IndicatorAnchorMax; [JsonProperty(PropertyName = "Visual indicator color")] public string IndicatorColor; [JsonProperty("Depth of an underground teleport (upon disconnection)")] public float UndergroundTeleportDepth; [JsonProperty(PropertyName = "Block all incoming damage while vanished (true/false)")] public bool BlockAllIncomingDamage; [JsonProperty(PropertyName = "Block all outgoing damage while vanished (true/false)")] public bool BlockAllOutgoingDamage; [JsonProperty(PropertyName = "Auto vanish on connect (true/false)")] public bool AutoVanish; [JsonProperty(PropertyName = "Auto noclip on connect (true/false)")] public bool AutoNoclip; [JsonProperty(PropertyName = "Persist vanish (don't unhide upon leave & restore after restart)")] public bool PersistVanish; [JsonProperty(PropertyName = "Use sound effects (true/false)")] public bool SoundEffects; [JsonProperty(PropertyName = "Enable safepoints (true/false)")] public bool SafePoints; [JsonProperty(PropertyName = "Remove all safepoints after wipe (true/false)")] public bool SafePointsRemoval; [JsonProperty(PropertyName = "Config revision (do not edit)")] public int ConfigRev; public static Configuration DefaultConfig() { return new Configuration { ShowIndicator = true, IndicatorAddress = "https://i.imgur.com/dOvX4uA.png", IndicatorColor = "0.7 0 0 1", IndicatorAnchorMin = "0.1 0.001", IndicatorAnchorMax = "0.17 0.10", UndergroundTeleportDepth = 15, BlockAllIncomingDamage = true, BlockAllOutgoingDamage = true, AutoVanish = true, AutoNoclip = true, PersistVanish = true, SoundEffects = true, SafePoints = true, SafePointsRemoval = true, ConfigRev = CfgRev }; } } private void MigrateConfig() { switch (_config.ConfigRev) { case 1: case 2: _config.ConfigRev = CfgRev; break; } SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if(_config.ConfigRev != CfgRev) MigrateConfig(); } catch { PrintWarning($"Could not read oxide/config/{Name}.json, creating new config file"); LoadDefaultConfig(); SaveConfig(); } } protected override void LoadDefaultConfig() => _config = Configuration.DefaultConfig(); protected override void SaveConfig() => Config.WriteObject(_config); #endregion #region Data private readonly ListHashSet _hiddenPlayers = new ListHashSet(); private readonly ListHashSet _hiddenPlayersPersist = new ListHashSet(); private Dictionary _safePoints; private void LoadSafePoints() => _safePoints = Interface.Oxide.DataFileSystem.ReadObject>($"{nameof(BetterVanish)}-SafePoints"); private void SaveSafePoints() => Interface.Oxide.DataFileSystem.WriteObject($"{nameof(BetterVanish)}-SafePoints", _safePoints); private void LoadPersistPlayers() { var lst = Interface.Oxide.DataFileSystem.ReadObject>($"{nameof(BetterVanish)}-PersistPlr"); _hiddenPlayersPersist.Clear(); _hiddenPlayersPersist.AddRange(lst); } private void SavePersistPlayers() => Interface.Oxide.DataFileSystem.WriteObject($"{nameof(BetterVanish)}-PersistPlr", new List(_hiddenPlayersPersist)); private void LoadData() { LoadSafePoints(); LoadPersistPlayers(); } private void SaveData() { SaveSafePoints(); SavePersistPlayers(); } #endregion #region Classes private class VanishComponent : MonoBehaviour, IEntity { private BasePlayer _owner; private GameObject _dummyObj; private static readonly ListHashSet InterestedTriggers = new ListHashSet { typeof(TriggerWorkbench), typeof(TriggerLadder), typeof(TriggerParent), typeof(TriggerParentEnclosed), typeof(TriggerParentExclusion), typeof(TriggerMount) }; private void Awake() { _owner = GetComponent(); if(_owner.IsConnected) StartNetworkGroupsUpdate(); SetupDummyCollider(); } private void UpdateNetworkGroups() { if (_owner.IsConnected && !_owner.IsSpectating()) _owner.net.UpdateGroups(_owner.transform.position); } public void StopNetworkGroupsUpdate() => CancelInvoke(nameof(UpdateNetworkGroups)); public void StartNetworkGroupsUpdate() => InvokeRepeating(nameof(UpdateNetworkGroups), 3f, 3f); private void OnDestroy() { Entity.Unregister(_dummyObj); _owner.RemoveFromTriggers(); Destroy(_dummyObj); _dummyObj = null; _owner = null; } #region Trigger Subsystem public bool IsDestroyed => _owner.IsDestroyed; private void SetupDummyCollider() { _dummyObj = _owner.gameObject.CreateChild(); _dummyObj.name = "VanishTrig"; //_dummyObj.transform.localPosition = (_owner.eyes.rotation * Vector3.forward * .6f) + PlayerEyes.EyeOffset * .7f; _dummyObj.tag = "DeployVolumeIgnore"; Entity.Register(_dummyObj, this); //_dummyObj.layer = Layers.Trigger; _dummyObj.layer = (int)Layer.Player_Server; _dummyObj.AddComponent().isTrigger = true; } private IEnumerable GetValidTriggers(Collider collider) => collider.GetComponentsInParent(). Where(t=>t.InterestedInObject(_owner.gameObject) && InterestedTriggers.Contains(t.GetType())); private void OnTriggerEnter(Collider collider) { if(_owner.IsSpectating()) return; foreach (var trigger in GetValidTriggers(collider)) trigger.OnTriggerEnter(_owner.playerCollider); } private void OnTriggerExit(Collider collider) { if(_owner.IsSpectating()) return; foreach (var trigger in GetValidTriggers(collider)) trigger.OnTriggerExit(_owner.playerCollider); } #endregion } private class LootProxyController : FacepunchBehaviour { public BasePlayer lootsrc; public BasePlayer looter; public PlayerCorpse proxy; public void Init(BasePlayer lootSource, BasePlayer looterPlayer) { lootsrc = lootSource; looter = looterPlayer; } private void Awake() { proxy = GetComponent(); InvokeRepeating(LifeCheck, 1f, .65f); } private void PlayerStoppedLooting(BasePlayer player) => Destroy(this); private void LifeCheck() { if (lootsrc && looter && looter.IsConnected) return; Destroy(this); } private void OnDestroy() { proxy.containers = null; proxy.Kill(); } } #endregion #region Localization protected override void LoadDefaultMessages() { // English lang.RegisterMessages(new Dictionary { ["CommandVanish"] = "vanish", ["CommandSetVanish"] = "setvanish", ["CommandUnvanishAll"] = "unvanishall", ["CommandInvSpy"] = "invspy", ["VanishDisabled"] = "Vanish disabled", ["VanishEnabled"] = "Vanish enabled", ["VanishDisabledOther"] = "You disabled vanish on {0}", ["VanishEnabledOther"] = "You enabled vanish on {0}", ["UnvanishedPlayers"] = "You unvanished {0} players!", ["SetVanishHelp"] = "Syntax: /setvanish PlayerName", ["SafePointNotSet"] = "You haven\'t set your Safe Point!\nType /vanish safepoint to save your current position.\nYou will be automatically teleported there upon disconnection.", ["SafePointSaved"] = "Your current position is saved as a Safe Point!", ["MultiplePlayers"] = "Multiple players found!\nRefine your search or use Steam ID.", ["NothingInSight"] = "No players in sight", ["NoSuchPlayer"] = "No such player found ({0})", ["InvSpyLooting"] = "Looting: {0} ({1}).", }, this); } #endregion #region Initialization private void Init() { _instance = this; permission.RegisterPermission(PermUse, this); permission.RegisterPermission(PermUseOther, this); permission.RegisterPermission(PermUnvanish, this); permission.RegisterPermission(PermInvSpy, this); permission.RegisterPermission(PermSkipLocks, this); AddCommandAliases("CommandVanish", nameof(VanishCommand)); AddCommandAliases("CommandSetVanish", nameof(SetVanishCommand)); AddCommandAliases("CommandUnvanishAll", nameof(UnvanishAllCommand)); AddCommandAliases("CommandInvSpy", nameof(InvSpyCommand)); if (!_config.AutoNoclip && !_config.AutoVanish && !_config.PersistVanish) Unsubscribe(nameof(OnPlayerConnected)); LoadData(); ManageHooks(); _cui = new CuiElementContainer { new CuiElement { Name = CuiName, Parent = "Hud.Menu", Components = { new CuiRawImageComponent { Color = _config.IndicatorColor, Url = _config.IndicatorAddress, Sprite = "assets/icons/refresh.png" }, new CuiRectTransformComponent { AnchorMin = _config.IndicatorAnchorMin, AnchorMax = _config.IndicatorAnchorMax } } } }; } private void OnServerInitialized() { var pp = GameManager.server.FindPrefab(PlayerPrefab).GetComponent().ToPlayer(); _drownEff = pp.drownEffect; _fallDmgEff = pp.fallDamageEffect; if (_config.PersistVanish) DisappearAll(_hiddenPlayersPersist); } private void OnNewSave(string _) { if (_config.SafePointsRemoval) _safePoints.Clear(); _hiddenPlayersPersist.Clear(); SaveData(); } #endregion #region De-initialization private void OnServerShutdown() => _isShutdown = true; private void Unload() { foreach (var uid in _hiddenPlayers.ToArray()) { var plr = FindPlayerById(uid); if(plr) HandleEvent(plr, VanishEvent.Unload); } SaveData(); if(!_isShutdown) ReappearAll(_config.PersistVanish ? _hiddenPlayersPersist : _hiddenPlayers); _config = null; _cui = null; _instance = null; } #endregion #region Commands private void VanishCommand(IPlayer iplayer, string command, string[] args) { var player = (BasePlayer)iplayer.Object; if (!player.IsAdmin && !HasPerm(player.UserIDString, PermUse)) return; if (args.Length > 0 && args[0].Equals("safepoint") && _config.SafePoints) { _safePoints[player.userID] = $"{player.transform.position.x} {player.transform.position.y} {player.transform.position.z}"; Player.Message(player, Lang("SafePointSaved", player.UserIDString)); return; } HandleEvent(player, IsPlayerVanished(player) ? VanishEvent.Reappear : VanishEvent.Disappear); } private void SetVanishCommand(IPlayer iplayer, string command, string[] args) { var player = (BasePlayer)iplayer.Object; if (!player.IsAdmin && !HasPerm(player.UserIDString, PermUseOther)) return; if (args.Length > 0) { var playerName = string.Join(" ", args); var result = FindPlayer(playerName); var target = result as BasePlayer; if (target == null && result != null) { Player.Message(player, Lang("MultiplePlayers", player.UserIDString)); return; } if (target == null) { Player.Message(player, Lang("NoSuchPlayer", player.UserIDString, playerName)); return; } var isVanished = IsPlayerVanished(target); HandleEvent(target, isVanished ? VanishEvent.Reappear : VanishEvent.Disappear); Player.Message(player, Lang(isVanished ? "VanishDisabledOther" : "VanishEnabledOther", player.UserIDString, target.displayName)); } else Player.Message(player, Lang("SetVanishHelp", player.UserIDString)); } private void UnvanishAllCommand(IPlayer iplayer, string command, string[] args) { var player = (BasePlayer)iplayer.Object; if (!player.IsAdmin && !HasPerm(player.UserIDString, PermUnvanish)) return; var count = 0; ReappearAll(_hiddenPlayers.Concat(_hiddenPlayersPersist), _=>count++); Player.Message(player, Lang("UnvanishedPlayers", player.UserIDString, count)); } private void InvSpyCommand(IPlayer iplayer, string command, string[] args) { var player = (BasePlayer)iplayer.Object; if (!player.IsAdmin && (!HasPerm(player.UserIDString, PermInvSpy))) return; timer.In(.15f, () => InvSpyShowLoot(player, args)); } #endregion #region Vanishing Act private void Disappear(BasePlayer player, bool showGui = true) { if (player.IsConnected) { if (!_hiddenPlayers.Contains(player.userID)) _hiddenPlayers.Add(player.userID); if (_config.PersistVanish) _hiddenPlayersPersist.Remove(player.userID); } else player.SetServerFall(false); player.syncPosition = false; player.limitNetworking = true; player.fallDamageEffect = EmptyObjRef; player.drownEffect = EmptyObjRef; player.GetHeldEntity()?.SetHeld(false); player.DisablePlayerCollider(); VanishComponent vc; if (player.gameObject.TryGetComponent(out vc)) vc.StartNetworkGroupsUpdate(); else player.gameObject.AddComponent(); SimpleAIMemory.AddIgnorePlayer(player); QueryGrid.Server.RemovePlayer(player); RemoveFromTargets(player); if (player.IsConnected) { if (showGui) VanishGui(player); Player.Message(player, Lang("VanishEnabled", player.UserIDString)); if (_config.SoundEffects) SendEffectTo(EffectDisappear, player); } ManageHooks(); plugins.CallHook("OnVanishDisappear", player); } #endregion #region Reappearing Act private void Reappear(BasePlayer player) { _hiddenPlayers.Remove(player.userID); _hiddenPlayersPersist.Remove(player.userID); player.syncPosition = true; player.limitNetworking = false; UnityEngine.Object.Destroy(player.GetComponent()); player.UpdateNetworkGroup(); player.SendNetworkUpdate(); player.GetHeldEntity()?.UpdateVisibility_Hand(); player.EnablePlayerCollider(); QueryGrid.Server.AddPlayer(player); SimpleAIMemory.RemoveIgnorePlayer(player); player.drownEffect = _drownEff; player.fallDamageEffect = _fallDmgEff; player.ForceUpdateTriggers(); if (player.IsConnected) { DestroyVanishGui(player); Player.Message(player, Lang("VanishDisabled", player.UserIDString)); if (_config.SoundEffects) SendEffectTo(EffectAppear, player); } ManageHooks(); plugins.CallHook("OnVanishReappear", player); } #endregion #region Hooks private object OnEntityTakeDamage(BaseEntity victim, HitInfo info) { var attacker = info?.InitiatorPlayer; if (victim == null && attacker == null) return null; var victimInvis = IsPlayerVanished(victim.ToPlayer()); var attackerInvis = IsPlayerVanished(attacker); if (!victimInvis && !attackerInvis) return null; if (victimInvis && !_config.BlockAllIncomingDamage) return null; if (attackerInvis && !victimInvis && !_config.BlockAllOutgoingDamage) return null; if (info == null) return this; info.damageTypes = EmptyDmgList; info.HitMaterial = 0; info.DoHitEffects = false; info.PointStart = Vector3.zero; info.HitEntity = null; return this; } private object OnEntityMarkHostile(BasePlayer player) => IsPlayerVanished(player) ? this : null; private object OnPlayerColliderEnable(BasePlayer player, CapsuleCollider _) => IsPlayerVanished(player) ? this : null; private object CanUseLockedEntity(BasePlayer player, BaseLock baseLock) { if (!IsPlayerVanished(player)) return null; if(player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermSkipLocks)) return TrueObj; var cl = baseLock as CodeLock; if (cl != null) return cl.whitelistPlayers.Contains(player.userID) || cl.guestPlayers.Contains(player.userID) ? TrueObj : FalseObj; return null; } private object OnPlayerViolation(BasePlayer player, AntiHackType type, float amount) => IsPlayerVanished(player) ? this : null; private object OnTeamInvite(BasePlayer inviter, BasePlayer target) => IsPlayerVanished(inviter) ? FalseObj : null; private void OnPlayerConnected(BasePlayer player) { if (player == null || !player.IsConnected) return; if(!player.IsAdmin && !HasPerm(player.UserIDString, PermUse)) return; if((_config.PersistVanish && IsPlayerPersisted(player)) || _config.AutoVanish) HandleEvent(player, VanishEvent.Disappear); if (_config.AutoNoclip) player.SendConsoleCommand("noclip"); } private void OnPlayerDisconnected(BasePlayer player) { if (!_hiddenPlayers.Contains(player.userID)) return; HandleEvent(player, VanishEvent.Disconnect); } #endregion #region GUI Indicator private static void VanishGui(BasePlayer player) { if (!_config.ShowIndicator) return; DestroyVanishGui(player); CuiHelper.AddUi(player, _cui); } private static void DestroyVanishGui(BasePlayer player) { if (!_config.ShowIndicator) return; CuiHelper.DestroyUi(player, CuiName); } #endregion #region Inventory Spy Logic private void InvSpyShowLoot(BasePlayer player, string[] args) { if (player.inventory.loot.IsLooting() || player.IsDead()) return; BasePlayer target = null; var playerName = args != null ? string.Join(" ", args) : null; if (string.IsNullOrWhiteSpace(playerName)) { var hits = Pool.GetList(); GamePhysics.TraceAll(player.eyes.HeadRay(), .1f, hits, 5f, Layers.Server.Players); var hit = hits.FirstOrDefault(h => h.collider.gameObject != player.gameObject); if (hit.collider != null) target = hit.transform.ToBaseEntity().ToPlayer(); Pool.FreeList(ref hits); } else { var result = FindPlayer(playerName); target = result as BasePlayer; if (target == null && result != null) { Player.Message(player, Lang("MultiplePlayers", player.UserIDString)); return; } } if (target == null) { Player.Message(player, $"{(string.IsNullOrWhiteSpace(playerName) ? Lang("NothingInSight", player.UserIDString) : Lang("NoSuchPlayer", player.UserIDString, playerName))}."); return; } var proxyCorpse = InvSpyCreateProxy(player, target); player.inventory.loot.StartLootingEntity(proxyCorpse, false); player.inventory.loot.AddContainer(target.inventory.containerMain); player.inventory.loot.AddContainer(target.inventory.containerWear); player.inventory.loot.AddContainer(target.inventory.containerBelt); player.inventory.loot.SendImmediate(); player.ClientRPCPlayer(null, player, "RPC_OpenLootPanel", "player_corpse"); Player.Message(player, Lang("InvSpyLooting", player.UserIDString, GetPrettyPlayerName(target), target.UserIDString)); } private static PlayerCorpse InvSpyCreateProxy(BasePlayer receiver, BasePlayer target) { var prx = (PlayerCorpse) GameManager.server.CreateEntity(PlayerCorpsePrefab, TerrainMeta.LowestPoint); prx.enableSaving = false; prx.syncPosition = false; prx.playerSteamID = receiver.userID; prx.playerName = GetPrettyPlayerName(target); prx.parentEnt = null; prx.Spawn(); prx.InitializeHealth(float.MaxValue, float.MaxValue); prx.gameObject.AddComponent().Init(target, receiver); UnityEngine.Object.Destroy(prx.GetComponent()); UnityEngine.Object.Destroy(prx.GetComponent()); prx.CancelInvoke(prx.RemoveCorpse); prx.SetFlag(BaseEntity.Flags.Busy, true); SendEntitySnapshotEx(receiver, prx); return prx; } #endregion #region Logic private void HandleEvent(BasePlayer player, VanishEvent evt) { switch (evt) { case VanishEvent.Disappear: Disappear(player); if (_config.SafePoints && !HasSafePoint(player) && player.IsConnected && !player.IsSleeping() && HasPerm(player.UserIDString, PermUse)) Player.Message(player, Lang("SafePointNotSet", player.UserIDString)); break; case VanishEvent.Reappear: Reappear(player); break; case VanishEvent.Disconnect: NextTick(()=>player.SetServerFall(false)); if ((player.IsAdmin || (_config.SafePoints && HasSafePoint(player))) && _config.UndergroundTeleportDepth != 0f) player.Teleport(GetSafePoint(player)); if (CanPersistPlayer(player)) { AddPersistPlayer(player); player.GetComponent()?.StopNetworkGroupsUpdate(); NextTick(()=>player.DisablePlayerCollider()); } else Reappear(player); break; case VanishEvent.Unload: if (CanPersistPlayer(player)) AddPersistPlayer(player); break; } } private static void RemoveFromTargets(BasePlayer player) { var hits = QueryGrid.Server.GetInSphere(player.GetNetworkPosition(), 64f, AIBrainSenses.queryResults, ent => ent is HumanNPC || ent is BaseAnimalNPC || ent is BaseFishNPC || ent is BradleyAPC); for (var i = 0; i < hits; i++) { var ent = AIBrainSenses.queryResults[i]; if(ent is HumanNPC) RemoveFromTargets0(player, ((HumanNPC)ent).Brain); else if(ent is BaseAnimalNPC) RemoveFromTargets0(player, ((BaseAnimalNPC)ent).brain); else if(ent is BaseFishNPC) RemoveFromTargets0(player, ent.GetComponent>()); else if(ent is BradleyAPC) { var apc = ((BradleyAPC)ent); var ti = apc.targetList.FirstOrDefault(t => t.entity == player); if (ti != null) { apc.targetList.Remove(ti); Pool.Free(ref ti); apc.UpdateTargetList(); } } } } private static void RemoveFromTargets0(BasePlayer player, BaseAIBrain brain) where T : BaseEntity { if(brain == null) return; try { if(brain.Events != null && brain.Events.Memory.Entity.Get(brain.Events.CurrentInputMemorySlot) == player) brain.Events.Memory.Entity.Remove(brain.Events.CurrentInputMemorySlot); brain.Senses.Memory.Players.Remove(player); brain.Senses.Memory.Targets.Remove(player); brain.Senses.Memory.Threats.Remove(player); brain.Senses.Memory.LOS.Remove(player); brain.Senses.Memory.All.RemoveAll(si => si.Entity == player); } catch (Exception e) { _instance.PrintError( $"RemoveFromTargets failure ({e.GetType()})! Brain: {brain.GetType().FullName}. [E:{brain.Events != null};S:{brain.Senses != null};SM:{brain.Senses?.Memory != null}]. Please report to us!"); } } private void ReappearAll(IEnumerable lst, Action onReappear = null) { foreach (var uid in lst) { var hiddenPlayer = FindPlayerById(uid); if (!hiddenPlayer) continue; Reappear(hiddenPlayer); onReappear?.Invoke(hiddenPlayer); } } private void DisappearAll(IEnumerable lst, Action onDisappear = null) { foreach (var uid in lst) { var hiddenPlayer = FindPlayerById(uid); if (!hiddenPlayer || (!hiddenPlayer.IsAdmin && !HasPerm(hiddenPlayer.UserIDString, PermUse))) continue; Disappear(hiddenPlayer); onDisappear?.Invoke(hiddenPlayer); } } private bool CanPersistPlayer(BasePlayer player) => _config.PersistVanish && (player.IsAdmin || HasPerm(player.UserIDString, PermUse)); private void AddPersistPlayer(BasePlayer player) { if(!_hiddenPlayersPersist.Contains(player.userID)) _hiddenPlayersPersist.Add(player.userID); _hiddenPlayers.Remove(player.userID); ManageHooks(); } #endregion #region Helpers private void AddCommandAliases(string key, string command) { foreach (var language in lang.GetLanguages(this)) { var messages = lang.GetMessages(language, this); foreach (var message in messages.Where(m => m.Key.Equals(key))) AddCovalenceCommand(message.Value, command); } } private void SubscribeToHooks() { foreach (var hook in HooksLst) Subscribe(hook); } private void UnSubscribeFromHooks() { foreach (var hook in HooksLst) Unsubscribe(hook); } private static void SendEffectTo(string effect, BasePlayer player) { EffectInstance.Init(Effect.Type.Generic, player, 0, Vector3.up, Vector3.zero); EffectInstance.pooledstringid = StringPool.Get(effect); Net.sv.write.Start(); Net.sv.write.PacketID(Message.Type.Effect); EffectInstance.WriteToStream(Net.sv.write); Net.sv.write.Send(new SendInfo(player.net.connection)); EffectInstance.Clear(); } private bool HasSafePoint(BasePlayer player) => _config.SafePoints && _safePoints.ContainsKey(player.userID); private Vector3 GetSafePoint(BasePlayer player) { try { if (HasSafePoint(player)) return Vector3Ex.Parse(_safePoints[player.userID]); } catch { } return new Vector3(player.transform.position.x, TerrainMeta.HeightMap.GetHeight(player.transform.position) - _config.UndergroundTeleportDepth, player.transform.position.z); } private string Lang(string key, string id = null, params object[] args) => string.Format(lang.GetMessage(key, this, id), args); private bool HasPerm(string id, string perm) => permission.UserHasPermission(id, perm); private bool IsPlayerVanished(BasePlayer player) => player != null && _hiddenPlayers.Contains(player.userID); private bool IsPlayerVanished(ulong playerId) => _hiddenPlayers.Contains(playerId); private bool IsPlayerPersisted(BasePlayer player) => player != null && ((!player.IsConnected || !player.Connection.connected) && _hiddenPlayersPersist.Contains(player.userID)); private static object FindPlayer(string name, bool sleeping = true) { var player = BasePlayer.Find(name) ?? BasePlayer.FindSleeping(name); if (player != null) return player; var list = BasePlayer.activePlayerList.ToArray(); if (sleeping) list = list.Concat(BasePlayer.sleepingPlayerList.Values).ToArray(); var result = list.Where(p => p.displayName.StartsWith(name, StringComparison.InvariantCultureIgnoreCase) || p.displayName.Contains(name, CompareOptions.IgnoreCase)); var players = result as BasePlayer[] ?? result.ToArray(); if (players.Length == 0) return null; if (players.Length > 1) return players; return players[0]; } private static BasePlayer FindPlayerById(ulong uid) => BasePlayer.FindByID(uid) ?? BasePlayer.FindSleeping(uid); private static void SendEntitySnapshotEx(BaseNetworkable receiver, BaseNetworkable ent) { if (ent == null || ent.net == null || !Net.sv.write.Start()) return; ++receiver.net.connection.validate.entityUpdates; var saveInfo = new BaseNetworkable.SaveInfo { forConnection = receiver.net.connection, forDisk = false }; Net.sv.write.PacketID(Message.Type.Entities); Net.sv.write.UInt32(receiver.net.connection.validate.entityUpdates); ent.ToStreamForNetwork(Net.sv.write, saveInfo); Net.sv.write.Send(new SendInfo(receiver.net.connection)); } private static string GetPrettyPlayerName(BasePlayer player) { if (player.IsNpc) return $"*NPC* {player.ShortPrefabName}"; else if (!player.IsConnected) return $"*Offline* {player.displayName}"; else if (player.IsSleeping()) return $"*Sleeping* {player.displayName}"; return $"{player.displayName}"; } private void ManageHooks() { var newState = _hiddenPlayers.Count >= 1; if(newState == _lastHooksState) return; if(newState) SubscribeToHooks(); else UnSubscribeFromHooks(); _lastHooksState = newState; } #endregion #region API private bool IsInvisible(BasePlayer player) => IsPlayerVanished(player); private bool IsInvisible(IPlayer player) => IsInvisible((BasePlayer)player.Object); private void DoVanishDisappear(BasePlayer player) => HandleEvent(player, VanishEvent.Disappear); private void DoVanishReappear(BasePlayer player) => HandleEvent(player, VanishEvent.Reappear); #endregion } }