C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.Linq;
using Network;
using Network.Visibility;
using Newtonsoft.Json;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Player Hider", "birthdates", "1.3.0")]
[Description("Don't network players that are out of view")]
public class PlayerHider : RustPlugin
{
#region Behaviour
/// <summary>
/// Behaviour that handles the viewing of players
/// </summary>
private class ViewBehaviour : FacepunchBehaviour
{
/// <summary>
/// A set of our hidden players
/// </summary>
private readonly HashSet<BasePlayer> _hiddenPlayers = new HashSet<BasePlayer>();
private readonly List<Connection> _list = new List<Connection>();
private bool _lastChangeable;
private Vector3 _lastPos;
/// <summary>
/// Our parent player
/// </summary>
private BasePlayer _player;
public Group Group { get; set; }
private void Awake()
{
_player = GetComponent<BasePlayer>();
_list.Add(_player.Connection);
Group = new Group(Net.sv.visibility, Net.sv.TakeUID());
foreach (var networkable in BaseNetworkable.GlobalNetworkGroup.networkables)
Group.networkables.Add(networkable);
_player.net.subscriber.UnsubscribeAll();
_player.net.subscriber.Subscribe(Group);
_player.net.SwitchGroup(Group);
_player.SendNetworkGroupChange();
_player.SendNetworkUpdateImmediate();
Instance._idToBehaviour[_player.userID] = this;
}
public void OnDestroy()
{
foreach (var player in _hiddenPlayers) ShowPlayer(player);
Instance._idToBehaviour.Remove(_player.userID);
_player.UpdateNetworkGroup();
Net.sv.ReturnUID(Group.ID);
}
/// <summary>
/// Called when our parent player ticks
/// </summary>
public void Tick()
{
foreach (var player in BasePlayer.sleepingPlayerList)
{
// If this is us, continue
if (player.userID == _player.userID ||
_lastChangeable && _player.transform.position.Equals(_lastPos)) continue;
// Retrieve the distance between us and the target player
var distance = _player.Distance(player.transform.position);
// If the distance is out of the render distance (max distance) or is not above the minimum distance, continue
if (distance >= MaxDistance || distance <= Instance._config.MinimumDistance) continue;
// Calculate the direction and execute a raycast with an obstruction mask at the distance between the players to see if anything is blocking the view
var direction = (_player.transform.position - player.transform.position).normalized;
RaycastHit hit;
if (!Physics.Raycast(_player.eyes.position, direction, out hit, distance,
Instance.ObstructionMask,
QueryTriggerInteraction.Ignore) || IsFiring(player))
{
// If not obstructed, continue or show the player if hidden
if (!_hiddenPlayers.Remove(player)) continue;
ShowPlayer(player);
continue;
}
// Set if the object we hit can be modified
_lastChangeable = hit.transform.gameObject.IsOnLayer(Instance.ModifiableObstructionMask);
_lastPos = _player.transform.position;
// If obstructed and hidden, continue.Otherwise, hide the player.if (!_hiddenPlayers.Add(player)) continue;
player.OnNetworkSubscribersLeave(_list);
}
}
/// <summary>
/// Show this player
/// </summary>
/// <param name="player">Target player</param>
public void ShowPlayer(BasePlayer player)
{
if (player.IsDestroyed || !player.IsConnected) return;
Group.networkables.Add(player.net);
player.SendAsSnapshot(_player.Connection);
player.GetHeldEntity()?.SendAsSnapshot(_player.Connection);
_player.SendNetworkGroupChange();
_player.SendNetworkUpdateImmediate();
}
/// <summary>
/// Hide this player
/// </summary>
/// <param name="player">Target player</param>
public void HidePlayer(BasePlayer player)
{
Group.Leave(player.net);
player.OnNetworkSubscribersLeave(_list);
}
/// <summary>
/// Can our parent player see this player?
/// </summary>
/// <param name="player">Target player</param>
/// <returns>True if our parent player can see this player.False, otherwise.</returns>
public bool CanSee(BasePlayer player)
{
return !_hiddenPlayers.Contains(player);
}
/// <summary>
/// Kill this behaviour and show all players
/// </summary>
public void Kill()
{
DestroyImmediate(this);
}
}
#endregion
#region Variables
/// <summary>
/// Max render distance
/// </summary>
private const float MaxDistance = 500.0f;
/// <summary>
/// Layer mask that will obstruct a player
/// </summary>
private int ObstructionMask { get; } = LayerMask.GetMask("Construction", "World", "Terrain");
/// <summary>
/// The layer mask from <see cref="ObstructionMask" /> that are modifiable
/// </summary>
private int ModifiableObstructionMask { get; } = LayerMask.GetMask("Construction");
/// <summary>
/// Plugin instance
/// </summary>
private static PlayerHider Instance { get; set; }
private readonly IDictionary<ulong, ViewBehaviour> _idToBehaviour = new Dictionary<ulong, ViewBehaviour>();
#endregion
#region Hooks
[HookMethod("CanSee")]
private bool CanSee(BasePlayer player, BasePlayer target)
{
if (_config.ShowOnFire && IsFiring(target)) return true;
var component = target.GetComponent<ViewBehaviour>();
return component == null || component.CanSee(player);
}
private void OnNetworkGroupLeft(BasePlayer player, Group group)
{
// If is not limit networking (limbo) group, return
if (group.ID != 1U) return;
ViewBehaviour viewBehaviour;
if (!_idToBehaviour.TryGetValue(player.userID, out viewBehaviour)) return;
player.net.SwitchGroup(viewBehaviour.Group);
}
private static bool IsFiring(BasePlayer player)
{
var projectile = player.GetHeldEntity() as BaseProjectile;
return projectile != null && projectile.NextAttackTime - Time.time >= -1;
}
private void Init()
{
Instance = this;
foreach (var player in BasePlayer.activePlayerList) OnPlayerRespawned(player);
}
private void OnServerInitialized()
{
timer.Every(_config.CheckInterval, CheckPlayers);
// Map to new list
MapGroupSubscribers(new UpdateListHashSet<Networkable>(OnNetworkableChanged));
}
private void OnNetworkableChanged(Networkable networkable, bool removed)
{
var foundPlayer =
BasePlayer.activePlayerList.FirstOrDefault(basePlayer => basePlayer.net.ID == networkable.ID);
foreach (var basePlayer in BasePlayer.activePlayerList)
{
ViewBehaviour viewBehaviour;
if (!_idToBehaviour.TryGetValue(basePlayer.userID, out viewBehaviour) ||
foundPlayer != null && !viewBehaviour.CanSee(foundPlayer)) continue;
var group = viewBehaviour.Group;
if (removed) group.networkables.Remove(networkable);
else group.networkables.Add(networkable);
basePlayer.SendNetworkUpdateImmediate();
}
}
private static void CheckPlayers()
{
foreach (var player in BasePlayer.activePlayerList) player.GetComponent<ViewBehaviour>()?.Tick();
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList) player.GetComponent<ViewBehaviour>()?.Kill();
Instance = null;
MapGroupSubscribers(new ListHashSet<Networkable>());
}
private static void MapGroupSubscribers(ListHashSet<Networkable> newValue)
{
var old = BaseNetworkable.GlobalNetworkGroup.networkables;
BaseNetworkable.GlobalNetworkGroup.networkables = newValue;
foreach (var networkable in old) newValue.Add(networkable);
}
private void OnPlayerRespawned(BasePlayer player)
{
player.gameObject.AddComponent<ViewBehaviour>();
}
private class UpdateListHashSet<T> : ListHashSet<T>
{
private readonly Action<T, bool> _callback;
public UpdateListHashSet(Action<T, bool> callback)
{
_callback = callback;
}
public new void Add(T value)
{
base.Add(value);
_callback.Invoke(value, false);
}
public new bool Remove(T value)
{
var ret = base.Remove(value);
if (ret) _callback.Invoke(value, true);
return ret;
}
}
#endregion
#region Configuration & Localization
private ConfigFile _config;
protected override void LoadConfig()
{
base.LoadConfig();
_config = Config.ReadObject<ConfigFile>();
if (_config == null) LoadDefaultConfig();
}
protected override void LoadDefaultConfig()
{
_config = ConfigFile.DefaultConfig();
PrintWarning("Default configuration has been loaded.");
}
protected override void SaveConfig()
{
Config.WriteObject(_config);
}
private class ConfigFile
{
/// <summary>
/// The minimum distance a player needs to be before being hidden
/// </summary>
[JsonProperty("Minimum Distance")]
public float MinimumDistance { get; set; }
/// <summary>
/// Show a player if they're firing?
/// </summary>
[JsonProperty("Show a Player if Firing")]
public bool ShowOnFire { get; set; }
/// <summary>
/// Check interval for hiding & showing
/// </summary>
[JsonProperty("Check Interval (seconds)")]
public float CheckInterval { get; set; }
public static ConfigFile DefaultConfig()
{
return new ConfigFile
{
MinimumDistance = 100f,
ShowOnFire = true,
CheckInterval = 0.5f
};
}
}
#endregion
}
}