Иконка ресурса

FREE HandcuffLimiter : 0.8.5 0.8.5

Нет прав для скачивания
C#:
/*
 * HandcuffLimiter
 * Rust uMod plugin that prevents restraint abuse by enforcing maximum handcuff duration and safely teleporting victims to Outpost or Bandit Camp.
 * Also gives captives a unique opportunity to punish or forgive their captors.
 *
 * Copyright (C) 2026 SeesAll
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * Commercial Licensing:
 * While this software is available under GPLv3 for open-source use,
 * commercial redistribution, resale, bundling in paid packages,
 * or closed-source modifications require a separate commercial license.
 *
 * For commercial licensing inquiries, contact:
 * (SeesAll on uMod | N01B4ME on Discord)
 */
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using Rust;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("HandcuffLimiter", "SeesAll", "0.8.5")]
    [Description("Prevents indefinite handcuff restraint by auto-releasing and teleporting restrained players to a safe monument after a configurable limit.")]
    public class HandcuffLimiter : RustPlugin {
#region Permissions
        private const string PermExempt = "handcufflimiter.exempt";
        private const string PermAdmin = "handcufflimiter.admin";
#endregion
#region Configuration
        private PluginConfig _config;
        private enum SafeDestination {
            Outpost = 1, BanditCamp = 2
        }
        private class PluginConfig {
            public bool Enabled = true;
            public int CheckIntervalSeconds = 2;
            public float ExemptPermissionCacheSeconds = 10f;
            // performance: cache exempt permission lookups for this many seconds
            public int MaxRestrainMinutes = 20;
            public int WarnSecondsBeforeLimit = 60;
            public bool WarnVictim = true;
            [JsonProperty("Teleport Destination (outpost / bandit)")]
            public string TeleportDestination = "outpost";
            // "outpost" or "bandit"
            [JsonProperty("Destination")]
            public string LegacyDestination;
            public bool ShouldSerializeLegacyDestination() => false;
            public Vector3 DestinationOffset = new Vector3(0f, 1.5f, 0f);
            public bool TeleportOnlyWithinSafeZone = true;
            public float SafeZoneSearchRadius = 25f;
            public int SafeZoneSearchAttempts = 8;
            public float TeleportClearanceRadius = 0.6f;
            public float TeleportClearanceHeight = 1.8f;
            public float BuildingProximityRejectRadius = 6f;
            public float TeleportAttemptTimeoutSeconds = 3.0f;
            public bool CacheSafeTeleportSpots = true;
            public float CacheBlacklistMinDistance = 6f;
            public int BlacklistMaxEntriesPerDestination = 50;
            public bool RemoveHoodBeforePrompt = false;
            public int HoodRemoveSecondsBeforeLimit = 60;
            public int VictimRecuffImmunitySeconds = 300;
            public int EpisodeMergeWindowSeconds = 60;
            public bool ChaosEnabled = false;
            public bool ChaosOnlyOutsideSafeZones = true;
            public int ChaosFuseSeconds = 10;
            public int ChaosCooldownMinutesPerVictim = 1440;
            public bool ChaosRadiusCheckPlayers = false;
            public float ChaosRadiusCheckMeters = 6f;
            public int ChaosMaxPlayersInRadius = 0;
            public bool EnablePunishForgivePrompt = false;
            public int PromptSecondsBeforeLimit = 60;
            public bool DebugEnabled = true;
            public int DebugTestSeconds = 60;
        }
        private SafeDestination ParseDestination(string value) {
            if (string.IsNullOrEmpty(value)) return SafeDestination.Outpost;
            var v = value.Trim().ToLowerInvariant();
            if (v == "1") return SafeDestination.Outpost;
            if (v == "2") return SafeDestination.BanditCamp;
            if (v == "outpost") return SafeDestination.Outpost;
            if (v == "bandit" || v == "banditcamp" || v == "bandit camp") return SafeDestination.BanditCamp;
            return SafeDestination.Outpost;
        }
        private int DestinationToInt() => (int)ParseDestination(_config?.TeleportDestination);
        protected override void LoadDefaultConfig() {
            _config = new PluginConfig();
            SaveConfig();
        }
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                _config = Config.ReadObject<PluginConfig>() ?? new PluginConfig();
                if (!string.IsNullOrEmpty(_config.LegacyDestination)) {
                    var legacyParsed = ParseDestination(_config.LegacyDestination);
                    if (string.IsNullOrEmpty(_config.TeleportDestination) || (_config.TeleportDestination.Trim().Equals("outpost", StringComparison.OrdinalIgnoreCase) && legacyParsed == SafeDestination.BanditCamp)) {
                        _config.TeleportDestination = (legacyParsed == SafeDestination.BanditCamp) ? "bandit" : "outpost";
                    }
                }
            }
            catch {
                PrintWarning("Config is invalid/corrupted; loading default values.");
                _config = new PluginConfig();
            }
            if (_config.MaxRestrainMinutes < 1) _config.MaxRestrainMinutes = 1;
            if (_config.CheckIntervalSeconds < 1) _config.CheckIntervalSeconds = 1;
            if (_config.CheckIntervalSeconds > 30) _config.CheckIntervalSeconds = 30;
            if (_config.WarnSecondsBeforeLimit < 0) _config.WarnSecondsBeforeLimit = 0;
            if (_config.ChaosFuseSeconds < 0) _config.ChaosFuseSeconds = 0;
            if (_config.SafeZoneSearchAttempts < 1) _config.SafeZoneSearchAttempts = 1;
            if (_config.SafeZoneSearchAttempts > 25) _config.SafeZoneSearchAttempts = 25;
            if (_config.SafeZoneSearchRadius < 0f) _config.SafeZoneSearchRadius = 0f;
            if (_config.TeleportClearanceRadius < 0.1f) _config.TeleportClearanceRadius = 0.1f;
            if (_config.TeleportClearanceRadius > 3f) _config.TeleportClearanceRadius = 3f;
            if (_config.TeleportClearanceHeight < 0.5f) _config.TeleportClearanceHeight = 0.5f;
            if (_config.TeleportClearanceHeight > 5f) _config.TeleportClearanceHeight = 5f;
            if (_config.PromptSecondsBeforeLimit < 0) _config.PromptSecondsBeforeLimit = 0;
            if (_config.DebugTestSeconds < 5) _config.DebugTestSeconds = 5;
            if (_config.HoodRemoveSecondsBeforeLimit < 0) _config.HoodRemoveSecondsBeforeLimit = 0;
            if (_config.CacheBlacklistMinDistance < 0f) _config.CacheBlacklistMinDistance = 0f;
            if (_config.TeleportAttemptTimeoutSeconds < 0f) _config.TeleportAttemptTimeoutSeconds = 0f;
            if (_config.ExemptPermissionCacheSeconds < 0f) _config.ExemptPermissionCacheSeconds = 0f;
            if (_config.BlacklistMaxEntriesPerDestination < 0) _config.BlacklistMaxEntriesPerDestination = 0;
            SaveConfig();
        }
        protected override void SaveConfig() => Config.WriteObject(_config, true);
#endregion
#region Lang
        private const string MsgVictimWarn = "VictimWarn";
        private const string MsgVictimAction = "VictimAction";
        private const string MsgAdminNoPerm = "AdminNoPerm";
        private const string MsgAdminUsage = "AdminUsage";
        private const string MsgAdminStatus = "AdminStatus";
        private const string MsgAdminNotFound = "AdminNotFound";
        private const string MsgAdminReset = "AdminReset";
        private const string MsgPromptTitle = "PromptTitle";
        private const string MsgPromptBody = "PromptBody";
        private const string MsgPromptPunish = "PromptPunish";
        private const string MsgPromptForgive = "PromptForgive";
        private const string MsgDebugStarted = "DebugStarted";
        private const string MsgDebugStopped = "DebugStopped";
        private const string MsgDebugDisabled = "DebugDisabled";
        private const string MsgCacheCleared = "CacheCleared";
        private const string MsgCacheUsage = "CacheUsage";
        private const string MsgWearListHeader = "WearListHeader";
        private const string MsgWearListLine = "WearListLine";
        private void Init() {
            permission.RegisterPermission(PermExempt, this);
            permission.RegisterPermission(PermAdmin, this);
            lang.RegisterMessages(new Dictionary<string, string> {
                [MsgVictimWarn]
                = "You have been restrained for a long time. If you are still restrained in {0} seconds, you will be released and moved to a safe location.", [MsgVictimAction] = "You were restrained too long. You have been released and moved to a safe location.", [MsgAdminNoPerm] = "You don't have permission to use that command.", [MsgAdminUsage] = "Usage: /hcl status <nameOrId>  OR  /hcl reset <nameOrId>  OR  /hcl debug  OR  /hcl debugoff  OR  /hcl clearcache [outpost|bandit|all]  OR  /hcl wear <nameOrId>", [MsgAdminNotFound] = "Player not found.", [MsgAdminStatus] = "{0} restrained={1}, tracked={2}, elapsed={3}s, debug={4}", [MsgAdminReset] = "Tracking state reset for {0}.", [MsgPromptTitle] = "Punish or Forgive?", [MsgPromptBody] = "You're about to be freed. Choose what happens when you teleport out.", [MsgPromptPunish] = "Punish", [MsgPromptForgive] = "Forgive", [MsgDebugStarted] = "Debug test started: you have been restrained for {0} seconds.", [MsgDebugStopped] = "Debug test stopped.", [MsgDebugDisabled] = "Debug mode is disabled in the config.", [MsgCacheUsage] = "Usage: /hcl clearcache [outpost|bandit|all]  OR  /hcl wear <nameOrId>", [MsgCacheCleared] = "Cleared cached safe teleport spot for: {0}. It will be re-learned on the next rescue.", ["WearListHeader"] = "Wear items for {0}:", ["WearListLine"] = "- {0} (itemid {1})"
            }
            , this);
        }
#endregion
#region Data
        private const int MaxLogEntries = 2000;
        private StoredData _data;
        private class StoredData {
            public string MapId;
            public Dictionary<ulong, long> LastChaosUtc = new Dictionary<ulong, long>();
            public Dictionary<int, SerializableVector3> CachedSafeSpots = new Dictionary<int, SerializableVector3>();
            public Dictionary<int, List<SerializableVector3>> BlacklistedSpots = new Dictionary<int, List<SerializableVector3>>();
            public List<LogEntry> Log = new List<LogEntry>();
        }
        private class SerializableVector3 {
            public float x, y, z;
            public SerializableVector3() {
            }
            public SerializableVector3(Vector3 v) {
                x = v.x;
                y = v.y;
                z = v.z;
            }
            public Vector3 ToVector3() => new Vector3(x, y, z);
        }
        private class LogEntry {
            public long Utc;
            public ulong VictimId;
            public string VictimName;
            public int DurationSeconds;
            public int Destination;
            public int Choice;
            public bool ChaosDropped;
        }
        private void LoadData() {
            _data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
            if (_data.LastChaosUtc == null) _data.LastChaosUtc = new Dictionary<ulong, long>();
            if (_data.Log == null) _data.Log = new List<LogEntry>();
            if (_data.CachedSafeSpots == null) _data.CachedSafeSpots = new Dictionary<int, SerializableVector3>();
            if (_data.BlacklistedSpots == null) _data.BlacklistedSpots = new Dictionary<int, List<SerializableVector3>>();
        }
        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, _data);
        private string GetCurrentMapId() {
            try {
                var seed = ConVar.Server.seed;
                var size = ConVar.Server.worldsize;
                var level = ConVar.Server.level ?? string.Empty;
                return $"{seed}:{size}:{level}";
            }
            catch {
                return "unknown";
            }
        }
        private void WipeData(string reason) {
            if (_data == null) _data = new StoredData();
            _data.MapId = GetCurrentMapId();
            _data.LastChaosUtc.Clear();
            _data.Log.Clear();
            _data.CachedSafeSpots.Clear();
            _data.BlacklistedSpots.Clear();
            SaveData();
            Puts($"Data wiped ({reason}).");
        }
#endregion
#region State
        private class RestrainState {
            public ulong VictimId;
            public double EpisodeStartUtc;
            public double LastSeenUnrestrainedUtc;
            public bool Warned;
            public bool IsCurrentlyRestrained;
            public bool PromptShown;
            public int Choice;
            public bool Debug;
            public int DebugMaxSeconds;
            public bool HoodRemoved;
        }
        private readonly Dictionary<ulong, RestrainState> _stateByVictim = new Dictionary<ulong, RestrainState>();
        private readonly Dictionary<ulong, double> _immuneUntilUtc = new Dictionary<ulong, double>();
        private struct PermCacheEntry {
            public bool Value;
            public double ExpiresUtc;
        }
        private readonly Dictionary<ulong, PermCacheEntry> _exemptPermCache = new Dictionary<ulong, PermCacheEntry>();
        private Timer _pollTimer;
        private readonly Dictionary<int, Vector3> _monumentCenterByDest = new Dictionary<int, Vector3>();
        private class PendingTeleport {
            public int Destination;
            public Vector3 Center;
            public int AttemptsRemaining;
            public float Radius;
            public bool TriedCached;
            public double StartedUtc;
        }
        private readonly Dictionary<ulong, PendingTeleport> _pendingTeleports = new Dictionary<ulong, PendingTeleport>();
        private const string UiLayer = "HandcuffLimiter.UI";
#endregion
#region Hooks
        private void OnServerInitialized() {
            if (!_config.Enabled) return;
            LoadData();
            var mapId = GetCurrentMapId();
            if (string.IsNullOrEmpty(_data.MapId) || !_data.MapId.Equals(mapId, StringComparison.Ordinal)) WipeData("map identity changed");
            CacheMonumentCenter(DestinationToInt());
            StartPolling();
        }
        private void OnNewSave(string filename) {
            if (_data != null) WipeData("OnNewSave");
        }
        private void Unload() {
            _pollTimer?.Destroy();
            var players = BasePlayer.activePlayerList;
            if (players != null) {
                for (var i = 0; i < players.Count; i++) {
                    var p = players[i];
                    if (p != null) DestroyPromptUi(p);
                }
            }
            _stateByVictim.Clear();
            _immuneUntilUtc.Clear();
            _monumentCenterByDest.Clear();
            _pendingTeleports.Clear();
            _exemptPermCache.Clear();
            if (_data != null) SaveData();
        }
        private void OnPlayerDisconnected(BasePlayer player, string reason) {
            if (player == null) return;
            DestroyPromptUi(player);
            _stateByVictim.Remove(player.userID);
            _immuneUntilUtc.Remove(player.userID);
            _pendingTeleports.Remove(player.userID);
            _exemptPermCache.Remove(player.userID);
        }
        private void OnPlayerDeath(BasePlayer player, HitInfo info) {
            if (player == null) return;
            DestroyPromptUi(player);
            _stateByVictim.Remove(player.userID);
            _immuneUntilUtc.Remove(player.userID);
            _pendingTeleports.Remove(player.userID);
            _exemptPermCache.Remove(player.userID);
        }
#endregion
#region Commands
        [ChatCommand("hcl")]
        private void CmdHcl(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin)) {
                player.ChatMessage(lang.GetMessage(MsgAdminNoPerm, this, player.UserIDString));
                return;
            }
            if (args == null || args.Length < 1) {
                player.ChatMessage(lang.GetMessage(MsgAdminUsage, this, player.UserIDString));
                return;
            }
            var sub = args[0].ToLowerInvariant();
            if (sub == "debug") {
                if (!_config.DebugEnabled) {
                    player.ChatMessage(lang.GetMessage(MsgDebugDisabled, this, player.UserIDString));
                    return;
                }
                StartDebugEpisode(player);
                player.ChatMessage(string.Format(lang.GetMessage(MsgDebugStarted, this, player.UserIDString), _config.DebugTestSeconds));
                return;
            }
            if (sub == "debugoff") {
                StopDebugEpisode(player);
                player.ChatMessage(lang.GetMessage(MsgDebugStopped, this, player.UserIDString));
                return;
            }
            if (sub == "clearcache") {
                var which = args.Length >= 2 ? args[1] : "all";
                if (!TryClearCache(which, out var cleared)) {
                    player.ChatMessage(lang.GetMessage(MsgCacheUsage, this, player.UserIDString));
                    return;
                }
                if (sub == "wear") {
                    if (args.Length < 2) {
                        player.ChatMessage(lang.GetMessage(MsgAdminUsage, this, player.UserIDString));
                        return;
                    }
                    var targetWear = FindPlayer(args[1]);
                    if (targetWear == null) {
                        player.ChatMessage(lang.GetMessage(MsgAdminNotFound, this, player.UserIDString));
                        return;
                    }
                    ShowWearList(player, targetWear);
                    return;
                }
                player.ChatMessage(string.Format(lang.GetMessage(MsgCacheCleared, this, player.UserIDString), cleared));
                return;
            }
            if (args.Length < 2) {
                player.ChatMessage(lang.GetMessage(MsgAdminUsage, this, player.UserIDString));
                return;
            }
            var target = FindPlayer(args[1]);
            if (target == null) {
                player.ChatMessage(lang.GetMessage(MsgAdminNotFound, this, player.UserIDString));
                return;
            }
            if (sub == "status") {
                var tracked = _stateByVictim.TryGetValue(target.userID, out var st);
                var restrained = (target.IsRestrained || IsWearingHandcuffs(target));
                var elapsed = tracked ? (int)Math.Floor(Interface.Oxide.Now - st.EpisodeStartUtc) : 0;
                var debug = tracked && st.Debug;
                player.ChatMessage(string.Format(lang.GetMessage(MsgAdminStatus, this, player.UserIDString), target.displayName, restrained, tracked, elapsed, debug));
                return;
            }
            if (sub == "reset") {
                DestroyPromptUi(target);
                _stateByVictim.Remove(target.userID);
                _immuneUntilUtc.Remove(target.userID);
                player.ChatMessage(string.Format(lang.GetMessage(MsgAdminReset, this, player.UserIDString), target.displayName));
                return;
            }
            player.ChatMessage(lang.GetMessage(MsgAdminUsage, this, player.UserIDString));
        }
        [ConsoleCommand("hcl.clearcache")]
        private void CCmdClearCache(ConsoleSystem.Arg arg) {
            var player = arg?.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin)) {
                arg.ReplyWith("No permission.");
                return;
            }
            var which = "all";
            if (arg?.Args != null && arg.Args.Length >= 1) which = arg.Args[0];
            if (!TryClearCache(which, out var cleared)) {
                arg.ReplyWith("Usage: hcl.clearcache <outpost|bandit|all>");
                return;
            }
            arg.ReplyWith($"Cleared cached safe teleport spot for: {cleared}. It will be re-learned on the next rescue.");
        }
        [ConsoleCommand("hcl.choice")]
        private void CmdChoice(ConsoleSystem.Arg arg) {
            var player = arg?.Player();
            if (player == null) return;
            if (arg.Args == null || arg.Args.Length < 1) return;
            if (!_stateByVictim.TryGetValue(player.userID, out var st)) return;
            var a = arg.Args[0].ToLowerInvariant();
            if (a == "punish") st.Choice = 1;
            else if (a == "forgive") st.Choice = 2;
            DestroyPromptUi(player);
        }
        private bool TryClearCache(string which, out string clearedText) {
            clearedText = string.Empty;
            if (_data == null) return false;
            which = (which ?? string.Empty).Trim().ToLowerInvariant();
            if (string.IsNullOrEmpty(which)) which = "all";
            var cleared = new List<string>(2);
            if (which == "all") {
                ClearCacheForDestination((int)SafeDestination.Outpost, cleared);
                ClearCacheForDestination((int)SafeDestination.BanditCamp, cleared);
            }
            else if (which == "outpost" || which == "1") {
                ClearCacheForDestination((int)SafeDestination.Outpost, cleared);
            }
            else if (which == "bandit" || which == "banditcamp" || which == "2") {
                ClearCacheForDestination((int)SafeDestination.BanditCamp, cleared);
            }
            else {
                return false;
            }
            clearedText = cleared.Count == 0 ? "none (no cached spot existed)" : string.Join(", ", cleared);
            return true;
        }
        private void ClearCacheForDestination(int destination, List<string> clearedList) {
            if (_data.CachedSafeSpots != null && _data.CachedSafeSpots.TryGetValue(destination, out var cached)) {
                AddToBlacklist(destination, cached.ToVector3());
                _data.CachedSafeSpots.Remove(destination);
                SaveData();
                clearedList.Add(destination == (int)SafeDestination.Outpost ? "Outpost" : "Bandit");
            }
            else {
            }
        }
        private BasePlayer FindPlayer(string nameOrId) {
            if (string.IsNullOrEmpty(nameOrId)) return null;
            if (ulong.TryParse(nameOrId, out var id)) {
                var p = BasePlayer.FindByID(id);
                if (p != null) return p;
                return BasePlayer.FindSleeping(id);
            }
            var lower = nameOrId.ToLowerInvariant();
            var list = BasePlayer.activePlayerList;
            for (var i = 0; i < list.Count; i++) {
                var p = list[i];
                if (p == null) continue;
                if (p.displayName != null && p.displayName.ToLowerInvariant().Contains(lower)) return p;
            }
            return null;
        }
        private bool IsExemptCached(BasePlayer player, double now) {
            if (player == null) return false;
            if (_config.ExemptPermissionCacheSeconds <= 0f) return permission.UserHasPermission(player.UserIDString, PermExempt);
            if (_exemptPermCache.TryGetValue(player.userID, out var entry) && entry.ExpiresUtc > now) return entry.Value;
            var value = permission.UserHasPermission(player.UserIDString, PermExempt);
            _exemptPermCache[player.userID] = new PermCacheEntry {
                Value = value, ExpiresUtc = now + _config.ExemptPermissionCacheSeconds
            }
            ;
            return value;
        }
        private void ShowWearList(BasePlayer admin, BasePlayer target) {
            if (admin == null || target == null) return;
            admin.ChatMessage(string.Format(lang.GetMessage(MsgWearListHeader, this, admin.UserIDString), target.displayName));
            try {
                var wear = target.inventory?.containerWear;
                if (wear == null || wear.itemList == null || wear.itemList.Count == 0) {
                    admin.ChatMessage("- (none)");
                    return;
                }
                var items = wear.itemList;
                for (var i = 0; i < items.Count; i++) {
                    var it = items[i];
                    if (it?.info == null) continue;
                    admin.ChatMessage(string.Format(lang.GetMessage(MsgWearListLine, this, admin.UserIDString), it.info.shortname, it.info.itemid));
                }
            }
            catch {
                admin.ChatMessage("- (error reading wear container)");
            }
        }
#endregion
#region Debug
        private void TryApplyDebugHandcuffs(BasePlayer player) {
            try {
                if (player == null) return;
                if (IsWearingHandcuffs(player)) return;
                var def = ItemManager.FindItemDefinition(HandcuffsShortname);
                if (def == null) {
                    PrintWarning("Debug: could not find ItemDefinition for handcuffs.");
                    return;
                }
                var item = ItemManager.Create(def, 1, 0UL);
                if (item == null) {
                    PrintWarning("Debug: could not create handcuffs item.");
                    return;
                }
                var wear = player.inventory?.containerWear;
                if (wear != null && item.MoveToContainer(wear)) return;
                item.Drop(player.transform.position + (Vector3.up * 0.25f), Vector3.zero);
                PrintWarning("Debug: handcuffs could not be worn automatically on this build. Dropped handcuffs at your feet; cuff yourself normally to test.");
            }
            catch {
            }
        }
        private void StartDebugEpisode(BasePlayer player) {
            if (player == null) return;
            TryApplyDebugHandcuffs(player);
            try {
                player.SendNetworkUpdateImmediate();
            }
            catch {
            }
            var now = Interface.Oxide.Now;
            if (!_stateByVictim.TryGetValue(player.userID, out var st)) {
                st = new RestrainState {
                    VictimId = player.userID
                }
                ;
                _stateByVictim[player.userID] = st;
            }
            st.EpisodeStartUtc = now;
            st.LastSeenUnrestrainedUtc = 0;
            st.Warned = false;
            st.IsCurrentlyRestrained = true;
            st.PromptShown = false;
            st.Choice = 0;
            st.Debug = true;
            st.DebugMaxSeconds = _config.DebugTestSeconds;
            st.HoodRemoved = false;
            _immuneUntilUtc.Remove(player.userID);
        }
        private void StopDebugEpisode(BasePlayer player) {
            if (player == null) return;
            DestroyPromptUi(player);
            _stateByVictim.Remove(player.userID);
            TryUnrestrain(player);
        }
#endregion
#region Core
        private void StartPolling() {
            _pollTimer?.Destroy();
            _pollTimer = timer.Every(_config.CheckIntervalSeconds, PollPlayers);
        }
        private void PollPlayers() {
            if (!_config.Enabled) return;
            var now = Interface.Oxide.Now;
            var normalMaxSeconds = _config.MaxRestrainMinutes * 60;
            var players = BasePlayer.activePlayerList;
            if (players == null || players.Count == 0) return;
            for (var i = 0; i < players.Count; i++) {
                var p = players[i];
                if (p == null || !p.IsConnected || p.IsDead()) continue;
                var tracked = _stateByVictim.TryGetValue(p.userID, out var st);
                var exempt = IsExemptCached(p, now);
                if (exempt && !(tracked && st.Debug)) {
                    DestroyPromptUi(p);
                    _stateByVictim.Remove(p.userID);
                    continue;
                }
                var restrained = (p.IsRestrained || IsWearingHandcuffs(p));
                if (restrained) {
                    if (_immuneUntilUtc.TryGetValue(p.userID, out var immuneUntil) && immuneUntil > now) {
                        TryUnrestrain(p);
                        continue;
                    }
                    if (!tracked) {
                        st = new RestrainState {
                            VictimId = p.userID, EpisodeStartUtc = now, LastSeenUnrestrainedUtc = 0, Warned = false, IsCurrentlyRestrained = true, PromptShown = false, Choice = 0, Debug = false, DebugMaxSeconds = 0, HoodRemoved = false
                        }
                        ;
                        _stateByVictim[p.userID] = st;
                    }
                    else {
                        if (!st.IsCurrentlyRestrained && _config.EpisodeMergeWindowSeconds > 0) {
                            var sinceUnrestrained = now - st.LastSeenUnrestrainedUtc;
                            if (sinceUnrestrained > _config.EpisodeMergeWindowSeconds) {
                                st.EpisodeStartUtc = now;
                                st.Warned = false;
                                st.PromptShown = false;
                                st.Choice = 0;
                                st.HoodRemoved = false;
                            }
                        }
                        st.IsCurrentlyRestrained = true;
                    }
                    var maxSeconds = st.Debug && st.DebugMaxSeconds > 0 ? st.DebugMaxSeconds : normalMaxSeconds;
                    var elapsed = now - st.EpisodeStartUtc;
                    var remaining = maxSeconds - elapsed;
                    if (_config.WarnVictim && !st.Warned && _config.WarnSecondsBeforeLimit > 0 && remaining <= _config.WarnSecondsBeforeLimit && remaining > 0) {
                        st.Warned = true;
                        p.ChatMessage(string.Format(lang.GetMessage(MsgVictimWarn, this, p.UserIDString), (int)Math.Ceiling(remaining)));
                    }
                    if (_config.RemoveHoodBeforePrompt && !st.HoodRemoved && _config.HoodRemoveSecondsBeforeLimit > 0 && remaining <= _config.HoodRemoveSecondsBeforeLimit && remaining > 0) {
                        st.HoodRemoved = true;
                        TryRemovePrisonerHood(p);
                    }
                    if (_config.EnablePunishForgivePrompt && !st.PromptShown && _config.PromptSecondsBeforeLimit > 0 && remaining <= _config.PromptSecondsBeforeLimit && remaining > 0) {
                        st.PromptShown = true;
                        ShowPromptUi(p);
                    }
                    if (elapsed >= maxSeconds) {
                        EnforceLimit(p, st, (int)Math.Floor(elapsed));
                    }
                }
                else {
                    if (tracked) {
                        if (st.IsCurrentlyRestrained) {
                            st.IsCurrentlyRestrained = false;
                            st.LastSeenUnrestrainedUtc = now;
                        }
                        else {
                            if (_config.EpisodeMergeWindowSeconds <= 0 || (now - st.LastSeenUnrestrainedUtc) > _config.EpisodeMergeWindowSeconds) {
                                DestroyPromptUi(p);
                                _stateByVictim.Remove(p.userID);
                                continue;
                            }
                        }
                        DestroyPromptUi(p);
                        if (st.Debug) {
                            _stateByVictim.Remove(p.userID);
                            continue;
                        }
                        if (_config.EpisodeMergeWindowSeconds <= 0) _stateByVictim.Remove(p.userID);
                    }
                }
            }
        }
        private void EnforceLimit(BasePlayer victim, RestrainState st, int durationSeconds) {
            if (victim == null || !victim.IsConnected) return;
            _stateByVictim.Remove(victim.userID);
            DestroyPromptUi(victim);
            var originPos = victim.transform.position;
            var originWasSafeZone = victim.InSafeZone();
            TryUnrestrain(victim);
            TeleportToDestination(victim, DestinationToInt());
            NextTick(() => TryUnrestrain(victim));
            victim.ChatMessage(lang.GetMessage(MsgVictimAction, this, victim.UserIDString));
            if (_config.VictimRecuffImmunitySeconds > 0 && !(st != null && st.Debug)) _immuneUntilUtc[victim.userID] = Interface.Oxide.Now + _config.VictimRecuffImmunitySeconds;
            var chaosDropped = false;
            if (_config.ChaosEnabled) {
                var shouldChaos = !_config.EnablePunishForgivePrompt || (st != null && st.Choice == 1);
                if (shouldChaos) chaosDropped = TryChaosDrop(victim.userID, originPos, originWasSafeZone);
            }
            AddLogEntry(victim, durationSeconds, st != null ? st.Choice : 0, chaosDropped);
            if (st != null && st.Debug) return;
            Puts($"Enforced restraint limit on {victim.displayName} ({victim.userID}). Destination={_config.TeleportDestination}.");
        }
        private void AddLogEntry(BasePlayer victim, int durationSeconds, int choice, bool chaosDropped) {
            if (_data == null) return;
            _data.Log.Add(new LogEntry {
                Utc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), VictimId = victim.userID, VictimName = victim.displayName, DurationSeconds = durationSeconds, Destination = DestinationToInt(), Choice = choice, ChaosDropped = chaosDropped
            }
            );
            while (_data.Log.Count > MaxLogEntries) _data.Log.RemoveAt(0);
            SaveData();
        }
#endregion
        private bool IsWearingHandcuffs(BasePlayer player) {
            try {
                var wear = player?.inventory?.containerWear;
                if (wear == null) return false;
                var items = wear.itemList;
                for (var i = 0; i < items.Count; i++) {
                    var it = items[i];
                    if (it?.info == null) continue;
                    if (it.info.itemid == HandcuffsItemId || string.Equals(it.info.shortname, HandcuffsShortname, StringComparison.OrdinalIgnoreCase)) return true;
                }
            }
            catch {
            }
            return false;
        }
        private bool TryRemoveWornHandcuffs(BasePlayer player) {
            try {
                var wear = player?.inventory?.containerWear;
                if (wear == null) return false;
                Item cuffs = null;
                var items = wear.itemList;
                for (var i = 0; i < items.Count; i++) {
                    var it = items[i];
                    if (it?.info == null) continue;
                    if (it.info.itemid == HandcuffsItemId || string.Equals(it.info.shortname, HandcuffsShortname, StringComparison.OrdinalIgnoreCase)) {
                        cuffs = it;
                        break;
                    }
                }
                if (cuffs == null) return false;
                cuffs.RemoveFromContainer();
                var main = player.inventory?.containerMain;
                if (main != null) {
                    if (cuffs.MoveToContainer(main)) return true;
                }
                cuffs.Drop(player.transform.position + (Vector3.up * 0.25f), player.estimatedVelocity);
                return true;
            }
            catch {
            }
            return false;
        }
        private void TryRemoveHandcuffsFromContainer(ItemContainer container) {
            if (container == null) return;
            try {
                var items = container.itemList;
                if (items == null || items.Count == 0) return;
                for (var i = items.Count - 1; i >= 0; i--) {
                    var it = items[i];
                    if (it?.info == null) continue;
                    if (it.info.itemid == HandcuffsItemId || string.Equals(it.info.shortname, HandcuffsShortname, StringComparison.OrdinalIgnoreCase)) {
                        it.RemoveFromContainer();
                        it.Remove();
                    }
                }
            }
            catch {
            }
        }
        private void TryClearRestrainedFlag(BasePlayer player) {
            if (player == null) return;
            try {
                var names = new[] {
                    "Restrained", "IsRestrained", "Handcuffed"
                }
                ;
                foreach (var n in names) {
                    try {
                        var flagObj = Enum.Parse(typeof(BasePlayer.PlayerFlags), n, true);
                        player.SetPlayerFlag((BasePlayer.PlayerFlags)flagObj, false);
                    }
                    catch {
                        /* ignore and try next */
                    }
                }
            }
            catch {
            }
        }
#region Prisoner Hood Removal
        private const string HandcuffsShortname = "handcuffs";
        private const int HandcuffsItemId = -839576748;
        private const int PrisonerHoodItemId = -892718768;
        private const string PrisonerHoodShortname = "prisonerhood";
        private void TryRemovePrisonerHood(BasePlayer player) {
            try {
                var wear = player?.inventory?.containerWear;
                if (wear == null) return;
                Item hood = null;
                var items = wear.itemList;
                for (var i = 0; i < items.Count; i++) {
                    var it = items[i];
                    if (it?.info == null) continue;
                    if (it.info.itemid == PrisonerHoodItemId || string.Equals(it.info.shortname, PrisonerHoodShortname, StringComparison.OrdinalIgnoreCase)) {
                        hood = it;
                        break;
                    }
                }
                if (hood == null) return;
                hood.RemoveFromContainer();
                hood.Drop(player.transform.position + (Vector3.up * 0.25f), player.estimatedVelocity);
            }
            catch {
            }
        }
#endregion
#region UI
        private void ShowPromptUi(BasePlayer player) {
            if (player == null) return;
            DestroyPromptUi(player);
            var container = new CuiElementContainer();
            container.Add(new CuiPanel {
                Image = {
                    Color = "0 0 0 0.65"
                }
                , RectTransform = {
                    AnchorMin = "0.35 0.35", AnchorMax = "0.65 0.60"
                }
                , CursorEnabled = true
            }
            , "Overlay", UiLayer);
            container.Add(new CuiLabel {
                Text = {
                    Text = lang.GetMessage(MsgPromptTitle, this, player.UserIDString), FontSize = 18, Align = TextAnchor.UpperCenter, Color = "1 1 1 1"
                }
                , RectTransform = {
                    AnchorMin = "0 0.65", AnchorMax = "1 0.98"
                }
            }
            , UiLayer);
            container.Add(new CuiLabel {
                Text = {
                    Text = lang.GetMessage(MsgPromptBody, this, player.UserIDString), FontSize = 14, Align = TextAnchor.UpperCenter, Color = "1 1 1 1"
                }
                , RectTransform = {
                    AnchorMin = "0.05 0.40", AnchorMax = "0.95 0.70"
                }
            }
            , UiLayer);
            container.Add(new CuiButton {
                Button = {
                    Command = "hcl.choice punish", Color = "0.75 0.20 0.20 0.9", Close = UiLayer
                }
                , RectTransform = {
                    AnchorMin = "0.07 0.08", AnchorMax = "0.47 0.32"
                }
                , Text = {
                    Text = lang.GetMessage(MsgPromptPunish, this, player.UserIDString), FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1"
                }
            }
            , UiLayer);
            container.Add(new CuiButton {
                Button = {
                    Command = "hcl.choice forgive", Color = "0.20 0.60 0.25 0.9", Close = UiLayer
                }
                , RectTransform = {
                    AnchorMin = "0.53 0.08", AnchorMax = "0.93 0.32"
                }
                , Text = {
                    Text = lang.GetMessage(MsgPromptForgive, this, player.UserIDString), FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1"
                }
            }
            , UiLayer);
            CuiHelper.AddUi(player, container);
        }
        private void DestroyPromptUi(BasePlayer player) {
            if (player == null) return;
            CuiHelper.DestroyUi(player, UiLayer);
        }
#endregion
#region Unrestrain
        private void TryUnrestrain(BasePlayer player) {
            if (player == null) return;
            TryRemoveWornHandcuffs(player);
            TryRemoveHandcuffsFromContainer(player?.inventory?.containerMain);
            TryRemoveHandcuffsFromContainer(player?.inventory?.containerBelt);
            TryClearRestrainedFlag(player);
            try {
                player.SendNetworkUpdateImmediate();
            }
            catch {
            }
            NextTick(() => {
                if (player == null || !player.IsConnected) return;
                if (player.IsRestrained || IsWearingHandcuffs(player)) {
                    TryRemoveWornHandcuffs(player);
                    TryRemoveHandcuffsFromContainer(player?.inventory?.containerMain);
                    TryRemoveHandcuffsFromContainer(player?.inventory?.containerBelt);
                    TryClearRestrainedFlag(player);
                    try {
                        player.SendNetworkUpdateImmediate();
                    }
                    catch {
                    }
                }
            }
            );
            timer.Once(0.2f, () => {
                if (player == null || !player.IsConnected) return;
                if (player.IsRestrained || IsWearingHandcuffs(player)) {
                    PrintWarning($"Unrestrain appears incomplete for {player.displayName} ({player.userID}): IsRestrained={player.IsRestrained}, WearingCuffs={IsWearingHandcuffs(player)}. If this persists, we can switch to a build-specific restraint clear routine.");
                }
            }
            );
        }
#endregion
#region Destination Teleport (with caching + blacklist)
        private void CacheMonumentCenter(int destination) {
            if (_monumentCenterByDest.ContainsKey(destination)) return;
            if (TryFindMonumentCenter(destination, out var center)) _monumentCenterByDest[destination] = center;
        }
        private bool TryFindMonumentCenter(int destination, out Vector3 pos) {
            pos = Vector3.zero;
            try {
                var monuments = TerrainMeta.Path?.Monuments;
                if (monuments == null || monuments.Count == 0) return false;
                var dest = (SafeDestination)destination;
                var primary = dest == SafeDestination.BanditCamp ? "bandit" : "outpost";
                var secondary = dest == SafeDestination.BanditCamp ? "town" : "compound";
                for (var i = 0; i < monuments.Count; i++) {
                    var m = monuments[i];
                    if (m == null) continue;
                    var name = m.name;
                    if (string.IsNullOrEmpty(name)) continue;
                    if (name.IndexOf(primary, StringComparison.OrdinalIgnoreCase) >= 0) {
                        pos = m.transform.position;
                        return true;
                    }
                }
                for (var i = 0; i < monuments.Count; i++) {
                    var m = monuments[i];
                    if (m == null) continue;
                    var name = m.name;
                    if (string.IsNullOrEmpty(name)) continue;
                    if (name.IndexOf(secondary, StringComparison.OrdinalIgnoreCase) >= 0) {
                        pos = m.transform.position;
                        return true;
                    }
                }
            }
            catch {
            }
            return false;
        }
        private void TeleportToDestination(BasePlayer player, int destination) {
            if (player == null) return;
            CacheMonumentCenter(destination);
            if (!_monumentCenterByDest.TryGetValue(destination, out var center)) {
                PrintWarning($"Teleport failed: destination monument not found (dest={destination}, player={player.displayName}).");
                return;
            }
            if (_config.TeleportOnlyWithinSafeZone) {
                BeginSafeZoneTeleport(player, destination, center);
                return;
            }
            ForceTeleport(player, MakeGrounded(center));
        }
        private void BeginSafeZoneTeleport(BasePlayer player, int destination, Vector3 center) {
            if (player == null) return;
            _pendingTeleports[player.userID] = new PendingTeleport {
                Destination = destination, Center = center, AttemptsRemaining = _config.SafeZoneSearchAttempts, Radius = _config.SafeZoneSearchRadius, TriedCached = false
            }
            ;
            AttemptNextSafeZoneCandidate(player);
        }
        private void AttemptNextSafeZoneCandidate(BasePlayer player) {
            if (player == null || !player.IsConnected) return;
            if (!_pendingTeleports.TryGetValue(player.userID, out var pending)) return;
            if (_config.TeleportAttemptTimeoutSeconds > 0f && (Interface.Oxide.Now - pending.StartedUtc) > _config.TeleportAttemptTimeoutSeconds) {
                _pendingTeleports.Remove(player.userID);
                _exemptPermCache.Remove(player.userID);
                ForceTeleport(player, MakeGrounded(pending.Center));
                return;
            }
            Vector3 candidate;
            var hasCandidate = false;
            if (_config.CacheSafeTeleportSpots && !pending.TriedCached && _data?.CachedSafeSpots != null && _data.Cach
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху