C#:
using System;
using System.Collections.Generic;
using System.IO;
using Facepunch;
using Network;
using Oxide.Core;
using Oxide.Core.Plugins;
using ProtoBuf;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("APAdminDaylight", "VORON", "0.0.2")]
[Description("Personal daylight via EnvSync entity snapshot (Facepunch blocks admintime from server)")]
public class APAdminDaylight : RustPlugin
{
private const string PermUse = "apadmindaylight.use";
private const float SyncIntervalSeconds = 5f;
private const float DaylightHour = 12f;
private readonly HashSet<ulong> _daylightPlayers = new HashSet<ulong>();
private EnvSync _envSync;
private GameObject _invokeHost;
private void Init()
{
permission.RegisterPermission(PermUse, this);
}
private void OnServerInitialized()
{
FindEnvSync();
StartSyncLoop();
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(
new Dictionary<string, string>
{
["NoPermission"] = "<color=#ff6666>Access denied.</color>",
["DaylightEnabled"] =
"<color=#a7ff83>Admin daylight enabled (personal, only for you).</color>",
["DaylightDisabled"] = "<color=#ffd479>Admin daylight disabled.</color>",
["NoEnvSync"] =
"<color=#ff6666>Daylight sync not ready — wait for world load and try again.</color>",
},
this,
"en"
);
lang.RegisterMessages(
new Dictionary<string, string>
{
["NoPermission"] = "<color=#ff6666>Нет доступа.</color>",
["DaylightEnabled"] =
"<color=#a7ff83>Админ-день включён (личный, только для вас).</color>",
["DaylightDisabled"] = "<color=#ffd479>Админ-день выключен.</color>",
["NoEnvSync"] =
"<color=#ff6666>Синхронизация дня ещё не готова — подождите загрузку мира и повторите.</color>",
},
this,
"ru"
);
}
private string Lang(BasePlayer player, string key)
{
return lang.GetMessage(key, this, player.UserIDString);
}
[ChatCommand("aday")]
private void CmdAdminDay(BasePlayer player, string cmd, string[] args)
{
if (player == null || !CanUse(player))
{
SendReply(player, Lang(player, "NoPermission"));
return;
}
if (_envSync == null)
FindEnvSync();
if (_envSync == null)
{
SendReply(player, Lang(player, "NoEnvSync"));
return;
}
if (_daylightPlayers.Contains(player.userID))
{
_daylightPlayers.Remove(player.userID);
RefreshLimitNetworking();
PushEnvToPlayer(player, lockDaylight: false);
SendReply(player, Lang(player, "DaylightDisabled"));
return;
}
_daylightPlayers.Add(player.userID);
RefreshLimitNetworking();
PushEnvToPlayer(player, lockDaylight: true);
SendReply(player, Lang(player, "DaylightEnabled"));
}
private bool CanUse(BasePlayer player)
{
return permission.UserHasPermission(player.UserIDString, PermUse)
|| player.IsAdmin
|| player.net?.connection?.authLevel >= 1;
}
private void FindEnvSync()
{
foreach (BaseNetworkable entity in BaseNetworkable.serverEntities)
{
if (entity is EnvSync sync)
{
_envSync = sync;
return;
}
}
}
private void StartSyncLoop()
{
if (_invokeHost != null)
{
UnityEngine.Object.Destroy(_invokeHost);
_invokeHost = null;
}
_invokeHost = new GameObject("APAdminDaylight.InvokeHost");
UnityEngine.Object.DontDestroyOnLoad(_invokeHost);
SyncDriver driver = _invokeHost.AddComponent<SyncDriver>();
driver.Initialize(this);
driver.InvokeRepeating(nameof(SyncDriver.Tick), 1f, SyncIntervalSeconds);
}
private void RefreshLimitNetworking()
{
if (_envSync == null || _envSync.IsDestroyed)
return;
// CHANGE: when anyone has personal daylight, we own EnvSync broadcasts (NightVision pattern)
_envSync.limitNetworking = _daylightPlayers.Count > 0;
}
internal void TickSyncAll()
{
if (_daylightPlayers.Count == 0)
return;
if (_envSync == null || _envSync.IsDestroyed)
{
FindEnvSync();
if (_envSync == null)
return;
}
RefreshLimitNetworking();
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (player?.IsConnected != true || player.net?.connection == null)
continue;
bool lockDaylight = _daylightPlayers.Contains(player.userID);
PushEnvToPlayer(player, lockDaylight);
}
}
private void PushEnvToPlayer(BasePlayer player, bool lockDaylight)
{
if (player?.IsConnected != true || player.net?.connection == null)
return;
if (_envSync == null || _envSync.IsDestroyed)
return;
if (!Net.sv.IsConnected())
return;
Connection connection = player.net.connection;
connection.validate.entityUpdates++;
NetWrite write = Net.sv.StartWrite();
write.PacketID(Message.Type.Entities);
write.UInt32(connection.validate.entityUpdates);
BaseNetworkable.SaveInfo saveInfo = new BaseNetworkable.SaveInfo
{
forConnection = connection,
forDisk = false,
};
// CHANGE: per-connection EnvSync snapshot — official path instead of blocked admintime
using (saveInfo.msg = Pool.Get<Entity>())
{
_envSync.Save(saveInfo);
if (lockDaylight && saveInfo.msg.environment != null)
{
DateTime dayDate =
TOD_Sky.Instance != null
? TOD_Sky.Instance.Cycle.DateTime.Date.AddHours(DaylightHour)
: new DateTime(2024, 1, 25, 12, 0, 0, DateTimeKind.Utc);
saveInfo.msg.environment.dateTime = dayDate.ToBinary();
saveInfo.msg.environment.fog = 0f;
saveInfo.msg.environment.rain = 0f;
saveInfo.msg.environment.clouds = 0f;
saveInfo.msg.environment.wind = 0f;
}
if (saveInfo.msg.baseEntity == null || saveInfo.msg.baseNetworkable == null)
{
Puts("[APAdminDaylight] EnvSync Save missing base entity data.");
return;
}
saveInfo.msg.WriteToStream((Stream)(object)write);
_envSync.PostSave(saveInfo);
}
write.Send(new SendInfo(connection));
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null)
return;
if (_daylightPlayers.Remove(player.userID))
RefreshLimitNetworking();
}
private void Unload()
{
if (_invokeHost != null)
{
UnityEngine.Object.Destroy(_invokeHost);
_invokeHost = null;
}
if (_envSync != null && !_envSync.IsDestroyed)
_envSync.limitNetworking = false;
_daylightPlayers.Clear();
_envSync = null;
}
private sealed class SyncDriver : MonoBehaviour
{
private APAdminDaylight _plugin;
public void Initialize(APAdminDaylight plugin) => _plugin = plugin;
public void Tick() => _plugin?.TickSyncAll();
}
}
}