using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries; using Oxide.Game.Rust.Cui; using UnityEngine; using UnityEngine.UI; namespace Oxide.Plugins { [Info("RaidHours", "Uzumi", "1.1.4")] [Description("Blocks or scales damage to base structures during configurable time windows.")] internal class RaidHours : RustPlugin { private const string PermissionCheck = "raidhours.check"; private const string PermissionBypass = "raidhours.bypass"; private const string PermissionAdmin = "raidhours.admin"; private const string MainPanelName = "UIRaidHoursAdmin"; private Configuration _config; private TimeZoneInfo _timeZone; private HashSet _protectedPrefabsSet; // prefab-navn som skal beskyttes private Dictionary _prefabCache = new Dictionary(); // prefabID -> er beskytta private Dictionary _entityBuilderCache = new Dictionary(); // netId -> bygger userID private Dictionary _notifyCooldownUntil = new Dictionary(); // cooldown for raid-melding til angripar private bool? _lastBlockState; // siste raid-block tilstand (for å oppdage overgang) private Timer _raidBlockTransitionTimer; private Timer _adminStatusRefreshTimer; private Dictionary _adminScrollToRow = new Dictionary(); private bool _adminOpenFromUiAction; private HashSet _adminPanelOpenForPlayers = new HashSet(); // Standard-liste over prefabs som skal ha raid-beskyttelse (dører, vegger, griller osv) private static readonly string[] DefaultProtectedPrefabs = new[] { "door.wood", "door.wood.double.hinged", "door.hinged.wood", "door.double.hinged.bardoors", "door.metal", "door.metal.double.hinged", "door.hinged.metal", "door.double.hinged.metal", "door.hinged.toptier", "door.double.hinged.toptier", "door.closer", "wall.external.high.stone", "wall.external.high.wood", "wall.external.high.ice", "wall.frame.cell.gate", "wall.frame.cell", "wall.frame.garagedoor", "wall.frame.shopfront", "wall.frame.shopfront.metal", "floor.grill", "floor.grill.reinforced", "floor.triangle.grill", "floor.ladder.hatch", "floor.triangle.ladder.hatch", "gate.external.high.stone", "gate.external.high.wood", "gates.external.high.stone", "gates.external.high.wood", "shutter.wood", "shutter.metal", "shutter.metal.embrasure.a", "shutter.metal.embrasure.b", "wall.window.bars.wood", "wall.window.bars.metal", "wall.window.bars.toptier", "wall.frame.netting", "lunar_new_year_2025" }; // Deployable-prefabs for tårn/SAM/feller (brukes når "Protect turrets and traps" e på) private static readonly string[] DeployablePrefabTurrets = new[] { "autoturret" }; private static readonly string[] DeployablePrefabSam = new[] { "samsite" }; private static readonly string[] DeployablePrefabShotgun = new[] { "guntrap" }; private static readonly string[] DeployablePrefabFlame = new[] { "flameturret" }; // Prefabs som aldri skal raid-blockast (spelar, tårn, osv.), sjølv med "Protect all deployables". private static readonly HashSet NeverRaidBlockProtectedPrefabNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "drone.deployed" }; private static bool IsNeverRaidBlockProtectedPrefabName(string name) { if (string.IsNullOrEmpty(name)) return false; string n = name.Replace('_', '.'); return NeverRaidBlockProtectedPrefabNames.Contains(name) || NeverRaidBlockProtectedPrefabNames.Contains(n); } private const string PrefabDataFileName = "RaidHours_Prefabs"; private PrefabData _prefabData; /// Datafil for beskytta prefabs og alle deployables - leses fra/skrives te data-mappen. private class PrefabData { [JsonProperty("Protected prefabs (base structures: doors, gates, walls, grills, etc.)")] public List ProtectedPrefabs = new List(); [JsonProperty("All deployable prefabs (used when 'Protect all deployables' is on)")] public List AllDeployablePrefabs = new List(); } // Vanlige tidssoner for admin-UI private static readonly string[] CommonTimeZoneIds = new[] { "Pacific/Honolulu", "America/Anchorage", "America/Los_Angeles", "America/Denver", "America/Chicago", "America/New_York", "America/St_Johns", "America/Sao_Paulo", "America/Buenos_Aires", "UTC", "Europe/London", "Europe/Paris", "Europe/Berlin", "Africa/Cairo", "Europe/Moscow", "Asia/Dubai", "Asia/Karachi", "Asia/Kolkata", "Asia/Bangkok", "Asia/Shanghai", "Asia/Tokyo", "Australia/Sydney", "Pacific/Auckland", "Pacific/Fiji" }; // Windows uses Windows timezone IDs, while admins often pick IANA IDs (like "America/New_York"). // If we cannot load the configured ID, try a best-effort IANA -> Windows mapping so DST works. private static readonly Dictionary CommonIanaToWindowsTimeZoneIds = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Pacific/Honolulu"] = "Hawaiian Standard Time", ["America/Anchorage"] = "Alaskan Standard Time", ["America/Los_Angeles"] = "Pacific Standard Time", ["America/Denver"] = "Mountain Standard Time", ["America/Chicago"] = "Central Standard Time", ["America/New_York"] = "Eastern Standard Time", ["America/St_Johns"] = "Newfoundland Standard Time", ["America/Sao_Paulo"] = "E. South America Standard Time", ["America/Buenos_Aires"] = "Argentina Standard Time", ["UTC"] = "UTC", ["Europe/London"] = "GMT Standard Time", ["Europe/Paris"] = "Romance Standard Time", ["Europe/Berlin"] = "W. Europe Standard Time", ["Africa/Cairo"] = "Egypt Standard Time", ["Europe/Moscow"] = "Russian Standard Time", ["Asia/Dubai"] = "Arabian Standard Time", ["Asia/Karachi"] = "Pakistan Standard Time", ["Asia/Kolkata"] = "India Standard Time", ["Asia/Bangkok"] = "SE Asia Standard Time", ["Asia/Shanghai"] = "China Standard Time", ["Asia/Tokyo"] = "Tokyo Standard Time", ["Australia/Sydney"] = "AUS Eastern Standard Time", ["Pacific/Auckland"] = "New Zealand Standard Time", ["Pacific/Fiji"] = "Fiji Standard Time" }; /// /// Resolves a CommonTimeZoneIds entry to TimeZoneInfo. On Windows, IANA ids fail unless mapped. /// private static bool TryResolveTimeZoneForCommonId(string timezoneId, out TimeZoneInfo tz) { tz = null; if (string.IsNullOrWhiteSpace(timezoneId)) return false; try { tz = TimeZoneInfo.FindSystemTimeZoneById(timezoneId); return true; } catch { } if (CommonIanaToWindowsTimeZoneIds.TryGetValue(timezoneId, out string windowsId) && !string.IsNullOrWhiteSpace(windowsId)) { try { tz = TimeZoneInfo.FindSystemTimeZoneById(windowsId); return tz != null; } catch { } } return false; } private static readonly double[] CommonTimeZoneOffsetHours = new[] { -10, -9, -8, -7, -6, -5, -3.5, -3, -3, 0, 0, 1, 1, 2, 3, 4, 5, 5.5, 7, 8, 9, 10, 12, 12 }; private static readonly string[] CommonTimeZoneDisplayNames = new[] { "UTC-10 Pacific/Honolulu", "UTC-9 America/Anchorage", "UTC-8 America/Los_Angeles", "UTC-7 America/Denver", "UTC-6 America/Chicago", "UTC-5 America/New_York", "UTC-3:30 America/St_Johns", "UTC-3 America/Sao_Paulo", "UTC-3 America/Buenos_Aires", "UTC (UTC)", "UTC Europe/London", "UTC+1 Europe/Paris", "UTC+1 Europe/Berlin", "UTC+2 Africa/Cairo", "UTC+3 Europe/Moscow", "UTC+4 Asia/Dubai", "UTC+5 Asia/Karachi", "UTC+5:30 Asia/Kolkata", "UTC+7 Asia/Bangkok", "UTC+8 Asia/Shanghai", "UTC+9 Asia/Tokyo", "UTC+10 Australia/Sydney", "UTC+12 Pacific/Auckland", "UTC+12 Pacific/Fiji" }; private double? _fallbackOffsetHours; // brukes viss system ikkje har timezone-id /// Hovudkonfig for RaidHours - tidssone, beskyttelsesvalg, meldingar, tidsvinduer private class Configuration { [JsonProperty("Timezone (IANA id, e.g. UTC or America/New_York)")] public string TimezoneId = "UTC"; [JsonProperty("Protect turrets, SAM sites, shotgun & flame traps")] public bool ProtectTurretsAndTraps = false; [JsonProperty("Protect all deployables (turrets, traps, SAM, etc.)")] public bool ProtectAllDeployables = false; [JsonProperty("Protect twig (building blocks at twig grade)")] public bool ProtectTwig = false; [JsonProperty("Protect vehicles (Modular boats & Tugboat, see whitelist/blacklist)")] public bool ProtectVehicles = false; [JsonProperty("Protected vehicle whitelist (prefabs protected when Protect vehicles is on). Move from blacklist to protect.")] public List ProtectedVehicleWhitelist = new List { "Tugboat", "Modularboat", "Modularcar" }; [JsonProperty("Unprotected vehicle blacklist (prefabs never protected). Move to whitelist to protect.")] public List UnprotectedVehicleBlacklist = new List { "Minicopter", "Scraptransporthelicopter", "Attackhelicopter", "Pedalbike", "Motorbike", "Motorbike_sidecar", "Snowmobile", "Pedaltrike", "Dpv.deployed", "Submarineduo.entity", "Kayak", "Ptboat", "Rhib", "Rowboat", "Submarinesolo.entity" }; [JsonProperty("Base owner = TC authed (on) or builder/teammate & Auth (off)")] public bool UseTCAuthForOwnership = false; [JsonProperty("Command for players to check raid times")] public string CommandRaid = "raid"; [JsonProperty("Command for admins to open panel")] public string CommandAdmin = "raidhours"; [JsonProperty("Require raidhours.check permission to use /raid (if false, everyone can use it)")] public bool RequirePermissionForRaidCommand = false; [JsonProperty("Notify attacker in chat when raid block applies (cooldown seconds; 0 = disabled; 60 = 1 per minute)")] public float NotifyAttackerCooldownSeconds = 60f; [JsonProperty("Steam ID used for chat icon (profile picture) on raid messages; empty = default icon")] public string ChatIconSteamId = "76561197963047047"; [JsonProperty("Announce in chat when raid block starts")] public bool AnnounceBlockStart = true; [JsonProperty("Announce in chat when raid block ends")] public bool AnnounceBlockEnd = true; [JsonProperty("Chat message when raid block starts ({pct} = block %, {duration} = time until end)")] public string MessageBlockStarted = "Raid block is now ACTIVE - {pct}% protection for {duration}"; [JsonProperty("Chat message when raid block ends")] public string MessageBlockEnded = "Raid block has ended - raiding is allowed"; [JsonProperty("Show game hint when raid block starts (duration and % protection)")] public bool ShowActiveHint = true; [JsonProperty("How long to show the game hint (seconds)")] public float HintDisplaySeconds = 8f; [JsonProperty("Game hint text when block starts ({duration} = time until end, {pct} = block %)")] public string MessageHint = "Raid block active for {duration} with {pct}% protection"; [JsonProperty("Show game hint when raid block ends")] public bool ShowEndHint = true; [JsonProperty("Game hint text when block ends")] public string MessageHintEnd = "Raid block has ended - raiding is allowed"; /// Eitt tidsvindue: start/slutt, kva dag, og block-prosent. public class TimeWindow { [JsonProperty("Start (time on start day when window opens)")] public string Start = "22:00"; [JsonProperty("End (defines duration: e.g. 17:00 to 11:00 = 18 hours from start)")] public string End = "06:00"; [JsonProperty("Day (start day: Everyday, WipeDay, or weekday; uses configured timezone)")] public string Day = "Everyday"; [JsonProperty("Block percent (0 = no block, 100 = full block)")] public int BlockPercent = 100; } [JsonProperty("Protected time windows (start and end in HH:mm, block % per window)")] public List ProtectedTimeWindows = new List(); } private static readonly string[] DayOptions = { "Everyday", "WipeDay", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; /// Lagar standard konfig og skriv han te fil (ny install / feil ved lesing). protected override void LoadDefaultConfig() { _config = new Configuration { TimezoneId = "UTC", ProtectTurretsAndTraps = false, ProtectAllDeployables = false, ProtectTwig = false, ProtectVehicles = false, ProtectedVehicleWhitelist = new List { "Tugboat", "Modularboat", "Modularcar" }, UnprotectedVehicleBlacklist = new List { "Minicopter", "Scraptransporthelicopter", "Attackhelicopter", "Pedalbike", "Motorbike", "Motorbike_sidecar", "Snowmobile", "Pedaltrike", "Dpv.deployed", "Submarineduo.entity", "Kayak", "Ptboat", "Rhib", "Rowboat", "Submarinesolo.entity" }, UseTCAuthForOwnership = false, CommandRaid = "raid", CommandAdmin = "raidhours", NotifyAttackerCooldownSeconds = 60f, ChatIconSteamId = "76561197963047047", AnnounceBlockStart = true, AnnounceBlockEnd = true, MessageBlockStarted = "Raid block is now ACTIVE - {pct}% protection for {duration}", MessageBlockEnded = "Raid block has ended - raiding is allowed", ShowActiveHint = true, HintDisplaySeconds = 8f, MessageHint = "Raid block active for {duration} with {pct}% protection", ShowEndHint = true, MessageHintEnd = "Raid block has ended - raiding is allowed", ProtectedTimeWindows = new List { new Configuration.TimeWindow { Start = "22:00", End = "06:00", Day = "Everyday", BlockPercent = 100 } } }; SaveConfig(); } /// Les konfig fra fil, validerer og fikser null/ugyldige verdier, så ApplyConfig. protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new Exception("Config null"); LoadPrefabData(); if (_config.ProtectedVehicleWhitelist == null) _config.ProtectedVehicleWhitelist = new List { "Tugboat", "Modularboat", "Modularcar" }; if (_config.UnprotectedVehicleBlacklist == null) _config.UnprotectedVehicleBlacklist = new List { "Minicopter", "Scraptransporthelicopter", "Attackhelicopter", "Pedalbike", "Motorbike", "Motorbike_sidecar", "Snowmobile", "Pedaltrike", "Dpv.deployed", "Submarineduo.entity", "Kayak", "Ptboat", "Rhib", "Rowboat", "Submarinesolo.entity" }; if (_config.ProtectedTimeWindows == null) _config.ProtectedTimeWindows = new List { new Configuration.TimeWindow { Start = "22:00", End = "06:00", Day = "Everyday", BlockPercent = 100 } }; bool allZeroBlock = _config.ProtectedTimeWindows.Count > 0 && _config.ProtectedTimeWindows.All(w => w.BlockPercent == 0); foreach (var w in _config.ProtectedTimeWindows) { if (string.IsNullOrWhiteSpace(w.Day)) w.Day = "Everyday"; if (allZeroBlock) w.BlockPercent = 100; else if (w.BlockPercent < 0 || w.BlockPercent > 100) w.BlockPercent = 100; } } catch (Exception ex) { Puts($"RaidHours config load error: {ex.Message}"); LoadDefaultConfig(); } ApplyConfig(); } /// Bruker konfig i minnet: set tidssone, bygger _protectedPrefabsSet og CachePrefabs. private void ApplyConfig() { _fallbackOffsetHours = null; try { _timeZone = TimeZoneInfo.FindSystemTimeZoneById(_config.TimezoneId); } catch { _timeZone = null; if (CommonIanaToWindowsTimeZoneIds.TryGetValue(_config.TimezoneId, out string windowsId) && !string.IsNullOrWhiteSpace(windowsId)) { try { _timeZone = TimeZoneInfo.FindSystemTimeZoneById(windowsId); } catch { _timeZone = null; } } if (_timeZone == null) { int idx = Array.IndexOf(CommonTimeZoneIds, _config.TimezoneId); if (idx >= 0 && idx < CommonTimeZoneOffsetHours.Length) _fallbackOffsetHours = CommonTimeZoneOffsetHours[idx]; else { _fallbackOffsetHours = 0; _config.TimezoneId = "UTC"; } } } var prefabList = (_prefabData?.ProtectedPrefabs != null && _prefabData.ProtectedPrefabs.Count > 0) ? new List(_prefabData.ProtectedPrefabs) : new List(DefaultProtectedPrefabs); if (_config.ProtectAllDeployables) { var fromFile = _prefabData?.AllDeployablePrefabs != null && _prefabData.AllDeployablePrefabs.Count > 0; if (fromFile) { foreach (var p in _prefabData.AllDeployablePrefabs) if (!string.IsNullOrWhiteSpace(p)) prefabList.Add(p.Trim()); } else { foreach (var name in GetDeployablePrefabNames()) prefabList.Add(name); } } else if (_config.ProtectTurretsAndTraps) { foreach (var p in DeployablePrefabTurrets) prefabList.Add(p); foreach (var p in DeployablePrefabSam) prefabList.Add(p); foreach (var p in DeployablePrefabShotgun) prefabList.Add(p); foreach (var p in DeployablePrefabFlame) prefabList.Add(p); } prefabList.RemoveAll(s => !string.IsNullOrWhiteSpace(s) && IsNeverRaidBlockProtectedPrefabName(s.Trim())); _protectedPrefabsSet = new HashSet(prefabList, StringComparer.OrdinalIgnoreCase); CachePrefabs(); } /// Skriv konfig-objektet te config-fil. protected override void SaveConfig() => Config.WriteObject(_config, true); /// Les prefab-data fra datafil (beskytta prefabs + alle deployables). Lagar standard viss fil manglar. private void LoadPrefabData() { try { _prefabData = Interface.Oxide.DataFileSystem.ReadObject(PrefabDataFileName); } catch { _prefabData = null; } if (_prefabData == null || _prefabData.ProtectedPrefabs == null || _prefabData.ProtectedPrefabs.Count == 0) { var deployableNames = GetDeployablePrefabNames(); var allDeployableList = deployableNames.Count > 0 ? deployableNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList() : new List(); _prefabData = new PrefabData { ProtectedPrefabs = new List(DefaultProtectedPrefabs), AllDeployablePrefabs = allDeployableList }; SavePrefabData(); } else if (_prefabData.AllDeployablePrefabs == null) { var deployableNames = GetDeployablePrefabNames(); _prefabData.AllDeployablePrefabs = deployableNames.Count > 0 ? deployableNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList() : new List(); } } /// Skriv prefab-data (beskytta + alle deployables) te datafil. private void SavePrefabData() { if (_prefabData == null) return; try { Interface.Oxide.DataFileSystem.WriteObject(PrefabDataFileName, _prefabData); } catch (Exception ex) { Puts($"Failed to save prefab data: {ex.Message}"); } } /// Hentar alle kortnamn på deployable-prefabs frå ItemManager (ting me kan plassere). private static HashSet GetDeployablePrefabNames() { var set = new HashSet(StringComparer.OrdinalIgnoreCase); if (ItemManager.itemList == null) return set; foreach (var item in ItemManager.itemList) { var mod = item?.GetComponent(); if (mod?.entityPrefab?.resourcePath == null) continue; var shortName = Path.GetFileNameWithoutExtension(mod.entityPrefab.resourcePath); if (!string.IsNullOrEmpty(shortName)) set.Add(shortName); } return set; } /// Fyller _prefabCache: for kvar deployable prefabID, om han e beskytta eller ikkje. private void CachePrefabs() { _prefabCache.Clear(); if (ItemManager.itemList == null) return; foreach (var item in ItemManager.itemList) { var mod = item?.GetComponent(); if (mod?.entityPrefab == null) continue; var baseEntity = mod.entityPrefab.GetEntity(); if (baseEntity == null) continue; var shortName = Path.GetFileNameWithoutExtension(mod.entityPrefab.resourcePath); if (string.IsNullOrEmpty(shortName)) continue; _prefabCache[baseEntity.prefabID] = IsProtectedPrefabByName(shortName); } } /// Registrerer permissions, chat-kommandoar, hooks og startar raid-block timeren. private void Init() { permission.RegisterPermission(PermissionCheck, this); permission.RegisterPermission(PermissionBypass, this); permission.RegisterPermission(PermissionAdmin, this); cmd.AddChatCommand(_config.CommandRaid, this, nameof(CmdRaid)); cmd.AddChatCommand(_config.CommandAdmin, this, nameof(CmdRaidHoursAdmin)); Subscribe(nameof(OnEntityBuilt)); Subscribe(nameof(OnItemDeployed)); Subscribe(nameof(OnEntityKill)); Subscribe(nameof(OnEntityTakeDamage)); Subscribe(nameof(OnStructureAttack)); Subscribe(nameof(OnPlayerDisconnected)); timer.Once(5f, StartRaidBlockTransitionTimer); _adminStatusRefreshTimer?.Destroy(); _adminStatusRefreshTimer = timer.Every(10f, RefreshAdminPanelStatusForAll); } /// Startar sjekk av raid-block overgang: først ein gang, så kvar 30 sekund. private void StartRaidBlockTransitionTimer() { if (_config == null) return; _lastBlockState = null; CheckRaidBlockTransition(); _raidBlockTransitionTimer?.Destroy(); _raidBlockTransitionTimer = timer.Every(30f, CheckRaidBlockTransition); } /// Ryddar opp ved unload: unhook, destroy timers, lukk UI, null ut cache og config. private void Unload() { Unsubscribe(nameof(OnEntityBuilt)); Unsubscribe(nameof(OnItemDeployed)); Unsubscribe(nameof(OnEntityKill)); Unsubscribe(nameof(OnEntityTakeDamage)); Unsubscribe(nameof(OnStructureAttack)); Unsubscribe(nameof(OnPlayerDisconnected)); _raidBlockTransitionTimer?.Destroy(); _raidBlockTransitionTimer = null; _adminStatusRefreshTimer?.Destroy(); _adminStatusRefreshTimer = null; DestroyBlockHintForAll(); foreach (var player in BasePlayer.activePlayerList) CloseAllRaidHoursUi(player); _prefabCache?.Clear(); _prefabCache = null; _entityBuilderCache?.Clear(); _entityBuilderCache = null; _notifyCooldownUntil?.Clear(); _notifyCooldownUntil = null; _adminScrollToRow?.Clear(); _adminScrollToRow = null; _adminPanelOpenForPlayers?.Clear(); _adminPanelOpenForPlayers = null; _adminOpenFromUiAction = false; _protectedPrefabsSet = null; _config = null; _timeZone = null; _prefabData = null; _lastBlockState = null; _fallbackOffsetHours = null; } /// Viss raid-block timeren ikkje vart starta under Init, start han no. private void OnServerInitialized() { if (_raidBlockTransitionTimer == null) timer.Once(2f, StartRaidBlockTransitionTimer); } /// Når nokon bygger building block: lagre bygger sin userID på entity netId i _entityBuilderCache. private void OnEntityBuilt(Planner plan, GameObject gameObject) { if (plan == null || gameObject == null) return; var player = plan.GetOwnerPlayer(); if (player == null || player.userID == 0) return; var buildingBlock = gameObject.GetComponent(); if (buildingBlock == null) return; ulong netId = buildingBlock.net?.ID.Value ?? 0UL; if (netId != 0) _entityBuilderCache[netId] = player.userID; } /// Når nokon plasserer deployable: lagre deployer sin userID på entity netId. private void OnItemDeployed(Deployer deployer, BaseEntity entity) { if (deployer == null || entity == null) return; var player = (deployer as Component)?.GetComponentInParent(); if (player == null || player.userID == 0) return; ulong netId = entity.net?.ID.Value ?? 0UL; if (netId != 0) _entityBuilderCache[netId] = player.userID; } /// Når entity blir drept: fjern han frå _entityBuilderCache. private void OnEntityKill(BaseEntity entity) { if (entity == null) return; ulong netId = entity.net?.ID.Value ?? 0UL; if (netId != 0) _entityBuilderCache.Remove(netId); } /// Når spelar disconnecter: fjern cooldown, scroll og admin-panel state for han. private void OnPlayerDisconnected(BasePlayer player, string reason) { if (player == null) return; ulong uid = player.userID; _notifyCooldownUntil?.Remove(uid); _adminScrollToRow?.Remove(uid); _adminPanelOpenForPlayers?.Remove(uid); } /// Lukkar all RaidHours-UI for spelaren (admin-panel, dropdowns, overlay). private void CloseAllRaidHoursUi(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, MainPanelName + "_day_scrollview"); CuiHelper.DestroyUi(player, MainPanelName + "_day_scroll"); CuiHelper.DestroyUi(player, MainPanelName + "_day"); CuiHelper.DestroyUi(player, MainPanelName + "_tz_scrollview"); CuiHelper.DestroyUi(player, MainPanelName + "_tz_scroll"); CuiHelper.DestroyUi(player, MainPanelName + "_tz"); CuiHelper.DestroyUi(player, MainPanelName); CuiHelper.DestroyUi(player, MainPanelName + "_overlay"); } /// Gir noverande tid i konfigurert tidssone (eller UTC + fallback offset). private DateTime GetCurrentTimeInZone() { if (_timeZone != null) return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _timeZone); return DateTime.UtcNow.AddHours(_fallbackOffsetHours ?? 0); } /// Hentar wipe-dato i UTC frå SaveRestore (viss tilgjengeleg). private static DateTime? GetWipeDateUtc() { var saveCreated = SaveRestore.SaveCreatedTime; if (saveCreated.Year >= 2000) return saveCreated.Kind == DateTimeKind.Utc ? saveCreated : saveCreated.ToUniversalTime(); return null; } /// Wipe-dato konvertert te konfigurert tidssone. private DateTime? GetWipeDateInConfigZone() { var wipeUtc = GetWipeDateUtc(); if (!wipeUtc.HasValue) return null; if (_timeZone != null) return TimeZoneInfo.ConvertTimeFromUtc(wipeUtc.Value, _timeZone); return wipeUtc.Value.AddHours(_fallbackOffsetHours ?? 0); } /// Sjekker om localNow sin dato e samme som wipe-dagen i konfig tidssone. private bool IsWipeDayToday(DateTime localNow) { var wipeInTz = GetWipeDateInConfigZone(); if (!wipeInTz.HasValue) return false; return localNow.Date == wipeInTz.Value.Date; } /// Parsar vindue start/slutt og gjer ut start-minutt og varighet i minutt. private static bool GetWindowDurationMinutes(Configuration.TimeWindow w, out int startM, out int durationMinutes) { startM = 0; durationMinutes = 0; if (w == null || !TryParseTime(w.Start, out startM) || !TryParseTime(w.End, out int endM)) return false; durationMinutes = endM >= startM ? (endM - startM) : (24 * 60 - startM + endM); return true; } /// Gir start-tidspunkt for vinduet som localNow ligg innanfor (WipeDay/Everyday/ukedag). private DateTime? GetCurrentWindowStart(Configuration.TimeWindow w, DateTime localNow) { if (w == null || string.IsNullOrWhiteSpace(w.Day)) return null; if (!GetWindowDurationMinutes(w, out int startM, out int durationM)) return null; string day = w.Day.Trim(); if (string.Equals(day, "WipeDay", StringComparison.OrdinalIgnoreCase)) { var wipeInTz = GetWipeDateInConfigZone(); if (!wipeInTz.HasValue) return null; DateTime windowStart = wipeInTz.Value.Date.AddMinutes(startM); DateTime windowEnd = windowStart.AddMinutes(durationM); if (localNow >= windowStart && localNow < windowEnd) return windowStart; return null; } if (string.Equals(day, "Everyday", StringComparison.OrdinalIgnoreCase)) { DateTime todayStart = localNow.Date.AddMinutes(startM); if (localNow >= todayStart && localNow < todayStart.AddMinutes(durationM)) return todayStart; DateTime yesterdayStart = localNow.Date.AddDays(-1).AddMinutes(startM); if (localNow >= yesterdayStart && localNow < yesterdayStart.AddMinutes(durationM)) return yesterdayStart; return null; } if (!Enum.TryParse(day, true, out DayOfWeek targetDay)) return null; int daysBack = ((int)localNow.DayOfWeek - (int)targetDay + 7) % 7; DateTime candidateDate = localNow.Date.AddDays(-daysBack); DateTime dowStart = candidateDate.AddMinutes(startM); DateTime dowEnd = dowStart.AddMinutes(durationM); if (localNow >= dowStart && localNow < dowEnd) return dowStart; return null; } /// Sjekker om localNow ligg innanfor dette tidsvinduet. private bool IsTimeInWindow(Configuration.TimeWindow w, DateTime localNow) { return GetCurrentWindowStart(w, localNow).HasValue; } /// Gir neste gang dette vinduet startar (for "starts in" / countdown). private DateTime? GetNextWindowStartDateTime(Configuration.TimeWindow w, DateTime localNow) { if (w == null || string.IsNullOrWhiteSpace(w.Day)) return null; if (!TryParseTime(w.Start, out int startM)) return null; string day = w.Day.Trim(); if (string.Equals(day, "WipeDay", StringComparison.OrdinalIgnoreCase)) { var wipeInTz = GetWipeDateInConfigZone(); if (!wipeInTz.HasValue) return null; DateTime windowStart = wipeInTz.Value.Date.AddMinutes(startM); if (localNow < windowStart) return windowStart; return null; } if (string.Equals(day, "Everyday", StringComparison.OrdinalIgnoreCase)) { DateTime todayStart = localNow.Date.AddMinutes(startM); if (localNow < todayStart) return todayStart; return localNow.Date.AddDays(1).AddMinutes(startM); } if (!Enum.TryParse(day, true, out DayOfWeek targetDay)) return null; int daysBack = ((int)localNow.DayOfWeek - (int)targetDay + 7) % 7; int daysToAdd; if (daysBack == 0) { if ((localNow.Hour * 60 + localNow.Minute) >= startM) daysToAdd = 7; else daysToAdd = 0; } else daysToAdd = 7 - daysBack; return localNow.Date.AddDays(daysToAdd).AddMinutes(startM); } /// Normaliserer tidsstreng til HH:mm (t.d. "2200" -> "22:00"). private static string NormalizeTimeInput(string s) { if (string.IsNullOrWhiteSpace(s)) return s?.Trim() ?? ""; s = s.Trim(); string digitsOnly = new string(s.Where(char.IsDigit).ToArray()); if (digitsOnly.Length == 4) return digitsOnly.Substring(0, 2) + ":" + digitsOnly.Substring(2, 2); return s; } /// Parsar tidsstreng (HH:mm eller 4 siffer) til totalt antal minutt frå midnatt. private static bool TryParseTime(string s, out int totalMinutes) { totalMinutes = 0; if (string.IsNullOrWhiteSpace(s)) return false; s = s.Trim(); var parts = s.Split(':'); int h, m; if (parts.Length >= 2) { if (!int.TryParse(parts[0], out h) || !int.TryParse(parts[1], out m)) return false; } else { string digitsOnly = new string(s.Where(char.IsDigit).ToArray()); if (digitsOnly.Length != 4) return false; if (!int.TryParse(digitsOnly.Substring(0, 2), out h) || !int.TryParse(digitsOnly.Substring(2, 2), out m)) return false; } h = ((h % 24) + 24) % 24; m = ((m % 60) + 60) % 60; totalMinutes = h * 60 + m; return true; } /// Om vinduet har spesifikk dag (ikkje "Everyday") - WipeDay eller ukedag. private static bool IsSpecificDay(Configuration.TimeWindow w) { if (w == null) return false; string day = w.Day?.Trim(); return !string.IsNullOrEmpty(day) && !string.Equals(day, "Everyday", StringComparison.OrdinalIgnoreCase); } /// Om minute ligg innanfor [startM, endM) (tar hensyn te at vindu kan gå over midnatt). private static bool TimeRangeContains(int startM, int endM, int minute) { if (startM < endM) return minute >= startM && minute < endM; if (startM > endM) return minute >= startM || minute < endM; return true; } /// Sjekker om to tidsintervall overlappar (over midnatt-vennleg). private static bool TimeRangesOverlap(int start1, int end1, int start2, int end2) { return TimeRangeContains(start1, end1, start2) || TimeRangeContains(start1, end1, end2) || TimeRangeContains(start2, end2, start1) || TimeRangeContains(start2, end2, end1); } /// Om me no ligg innanfor eit beskytta tidsvindue (uansett prosent). private bool IsInProtectedWindow(DateTime localNow) { return GetBlockPercentForCurrentWindow(localNow, out _); } /// Gir block-prosent for noverande vindue viss me ligg i eit; spesifikke dagar vinn over Everyday. private bool GetBlockPercentForCurrentWindow(DateTime localNow, out int blockPercent) { blockPercent = 0; foreach (var w in _config.ProtectedTimeWindows) { if (!IsTimeInWindow(w, localNow)) continue; if (IsSpecificDay(w)) { blockPercent = Mathf.Clamp(w.BlockPercent, 0, 100); return true; } } foreach (var w in _config.ProtectedTimeWindows) { if (!IsTimeInWindow(w, localNow)) continue; blockPercent = Mathf.Clamp(w.BlockPercent, 0, 100); return true; } return false; } /// Antal minutt til dette vinduet startar neste gang (eller int.MaxValue). private int GetMinutesUntilNextWindowStart(Configuration.TimeWindow w, DateTime localNow) { var next = GetNextWindowStartDateTime(w, localNow); if (!next.HasValue) return int.MaxValue; int minutes = (int)Math.Round((next.Value - localNow).TotalMinutes); return minutes > 0 ? minutes : int.MaxValue; } /// Gir noverande raid-block tilstand og kor lenge til den endrar seg (til chat/hint/UI). private void GetNextTransition(DateTime localNow, out bool currentlyProtected, out TimeSpan untilChange) { currentlyProtected = IsInProtectedWindow(localNow); int bestMinutesUntil = int.MaxValue; if (currentlyProtected) { Configuration.TimeWindow winningWindow = null; foreach (var w in _config.ProtectedTimeWindows) { if (!IsTimeInWindow(w, localNow)) continue; if (IsSpecificDay(w)) { winningWindow = w; break; } if (winningWindow == null) winningWindow = w; } if (winningWindow != null) { var start = GetCurrentWindowStart(winningWindow, localNow); if (start.HasValue && GetWindowDurationMinutes(winningWindow, out _, out int durationM)) { DateTime windowEnd = start.Value.AddMinutes(durationM); bestMinutesUntil = Math.Max(0, (int)Math.Round((windowEnd - localNow).TotalMinutes)); } } } else { foreach (var w in _config.ProtectedTimeWindows) { int until = GetMinutesUntilNextWindowStart(w, localNow); if (until > 0 && until < bestMinutesUntil) bestMinutesUntil = until; } } if (bestMinutesUntil == int.MaxValue) bestMinutesUntil = 0; untilChange = TimeSpan.FromMinutes(bestMinutesUntil); } /// Vinduer som skal visast for /raid: aktive no, utan duplikat der spesifikke dagar overlappar Everyday. private List GetWindowsToDisplayForRaid(DateTime localNow) { var activeNow = _config.ProtectedTimeWindows.Where(w => IsTimeInWindow(w, localNow)).ToList(); var result = new List(); foreach (var w in activeNow) { if (!GetWindowDurationMinutes(w, out _, out _)) continue; if (!IsSpecificDay(w)) { bool overlappedBySpecific = activeNow.Any(w2 => { if (!IsSpecificDay(w2)) return false; if (!GetWindowDurationMinutes(w2, out int s2, out int d2) || !GetWindowDurationMinutes(w, out int s1, out int d1)) return false; int e1 = (s1 + d1) % (24 * 60); int e2 = (s2 + d2) % (24 * 60); return TimeRangesOverlap(s1, e1, s2, e2); }); if (overlappedBySpecific) continue; } result.Add(w); } return result; } /// Om entity e køyretøy (BaseVehicle eller barn av køyretøy). private static bool IsVehicleEntity(BaseCombatEntity entity) { if (entity == null) return false; if (entity is BaseVehicle) return true; var parent = entity.GetParentEntity(); return parent != null && parent is BaseVehicle; } /// Hentar rot-køyretøyet (selv om entity e barn, t.d. modul). private static BaseVehicle GetRootVehicle(BaseCombatEntity entity) { if (entity == null) return null; if (entity is BaseVehicle v) return v; var parent = entity.GetParentEntity(); return parent as BaseVehicle; } /// Konfig-navn for køyretøy (Modularboat, Modularcar, Tugboat el. ShortPrefabName). private static string GetVehicleConfigName(BaseVehicle vehicle) { if (vehicle == null) return null; try { Type playerBoatType = typeof(BaseEntity).Assembly.GetType("PlayerBoat") ?? Type.GetType("PlayerBoat, Assembly-CSharp"); if (playerBoatType != null && playerBoatType.IsInstanceOfType(vehicle)) return "Modularboat"; } catch { } if (vehicle is ModularCar) return "Modularcar"; var children = vehicle.children; if (children != null) { for (int i = 0; i < children.Count; i++) { if (children[i] is VehiclePrivilege) return "Tugboat"; } } return GetShortPrefabName(vehicle as BaseCombatEntity); } /// Om køyretøyet står på blacklist (aldri beskytta). private bool IsVehicleInBlacklist(string configName) { if (string.IsNullOrEmpty(configName) || _config?.UnprotectedVehicleBlacklist == null) return false; return _config.UnprotectedVehicleBlacklist.Any(x => string.Equals(x, configName, StringComparison.OrdinalIgnoreCase)); } /// Om køyretøyet står på whitelist (kan beskyttast). private bool IsVehicleInWhitelist(string configName) { if (string.IsNullOrEmpty(configName) || _config?.ProtectedVehicleWhitelist == null) return false; return _config.ProtectedVehicleWhitelist.Any(x => string.Equals(x, configName, StringComparison.OrdinalIgnoreCase)); } /// Om køyretøyet skal ha raid-beskyttelse ifølge config (ikkje blacklist, på whitelist, modular = TC-sjekk). private bool IsVehicleProtectedByConfig(BaseCombatEntity entity, BaseVehicle vehicle, string configName) { if (IsVehicleInBlacklist(configName)) return false; if (!IsVehicleInWhitelist(configName)) return false; if (vehicle is ModularCar car) return IsModularCarProtectedByTC(entity, car); return true; } /// Modular car: alle på CarLock whitelist må vere authed på TC ved entity. private static bool IsModularCarProtectedByTC(BaseCombatEntity entity, ModularCar car) { if (car?.CarLock?.WhitelistPlayers == null || car.CarLock.WhitelistPlayers.Count == 0) return false; var priv = entity?.GetBuildingPrivilege(); if (priv == null || priv.IsDestroyed) return false; foreach (ulong ownerId in car.CarLock.WhitelistPlayers) { if (ownerId == 0) continue; if (!TcHasAuthorizedUserId(priv, ownerId)) return false; } return true; } private static Type _modularCarCodeLockType; /// Hentar ModularCarCodeLock-typen via reflection (for kodlås-whitelist). private static Type GetModularCarCodeLockType() { if (_modularCarCodeLockType != null) return _modularCarCodeLockType; _modularCarCodeLockType = typeof(BaseEntity).Assembly.GetType("ModularCarCodeLock") ?? Type.GetType("ModularCarCodeLock, Assembly-CSharp"); return _modularCarCodeLockType; } /// Sjekker om lock-objektet (kodlås) har userId på whitelist (reflection). private static bool WhitelistContainsUserId(object lockObj, ulong userId) { if (lockObj == null) return false; Type t = lockObj.GetType(); var prop = t.GetProperty("whitelistPlayers", BindingFlags.Public | BindingFlags.Instance) ?? t.GetProperty("WhitelistPlayers", BindingFlags.Public | BindingFlags.Instance) ?? t.GetProperty("authorizedPlayers", BindingFlags.Public | BindingFlags.Instance); if (prop?.GetValue(lockObj) is System.Collections.IList list) { for (int i = 0; i < list.Count; i++) { var entry = list[i]; if (entry is ulong u && u == userId) return true; if (entry != null && Convert.ToUInt64(entry) == userId) return true; } } return false; } /// Om spillaren står på køyretøyet sin kodlås-whitelist (ModularCarCodeLock). private static bool IsPlayerOnVehicleCodeLockWhitelist(BaseVehicle vehicle, ulong userId) { if (vehicle == null) return false; Type lockType = GetModularCarCodeLockType(); if (lockType == null) return false; var component = vehicle as Component; if (component == null) return false; var locks = component.GetComponentsInChildren(lockType); if (locks != null) { for (int i = 0; i < locks.Length; i++) { if (WhitelistContainsUserId(locks[i], userId)) return true; } } var children = vehicle.children; if (children == null) return false; for (int i = 0; i < children.Count; i++) { var childComp = children[i] as Component; if (childComp == null) continue; var childLocks = childComp.GetComponentsInChildren(lockType); if (childLocks == null) continue; for (int j = 0; j < childLocks.Length; j++) { if (WhitelistContainsUserId(childLocks[j], userId)) return true; } } return false; } /// Om angriparen sitter på køyretøyet (mountPoints). private static bool IsAttackerMountedOnVehicle(BaseVehicle vehicle, BasePlayer attacker) { if (vehicle == null || attacker == null) return false; try { var mountPoints = vehicle.mountPoints; if (mountPoints == null) return false; foreach (var mountPoint in mountPoints) { var mounted = mountPoint.mountable?.GetMounted(); if (mounted == attacker) return true; } } catch { } return false; } /// Om spillaren e authed på PlayerBoat (reflection IsPlayerAuthed). private static bool IsPlayerAuthedOnPlayerBoat(BaseVehicle vehicle, BasePlayer player) { if (vehicle == null || player == null) return false; try { Type playerBoatType = typeof(BaseEntity).Assembly.GetType("PlayerBoat") ?? Type.GetType("PlayerBoat, Assembly-CSharp"); if (playerBoatType == null || !playerBoatType.IsInstanceOfType(vehicle)) return false; var method = playerBoatType.GetMethod("IsPlayerAuthed", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(BasePlayer), typeof(bool) }, null); if (method == null) return false; var result = method.Invoke(vehicle, new object[] { player, true }); return result is bool b && b; } catch { return false; } } /// Om angriparen e autorisert på køyretøyet (boat auth, mounted, TC, owner, kodlås, CarLock, Tugboat auth). private static bool IsAttackerVehicleAuthorized(BaseCombatEntity entity, BasePlayer attacker) { if (attacker == null) return false; var vehicle = GetRootVehicle(entity); if (vehicle == null) return false; if (IsPlayerAuthedOnPlayerBoat(vehicle, attacker)) return true; if (IsAttackerMountedOnVehicle(vehicle, attacker)) return true; var buildingPriv = entity?.GetBuildingPrivilege(); if (buildingPriv != null && !buildingPriv.IsDestroyed && buildingPriv.IsAuthed(attacker)) return true; if (vehicle is BaseEntity vehicleEntity && vehicleEntity.OwnerID != 0 && vehicleEntity.OwnerID == attacker.userID) return true; if (IsPlayerOnVehicleCodeLockWhitelist(vehicle, attacker.userID)) return true; if (vehicle is ModularCar modularCar) { if (modularCar.CarLock != null && modularCar.CarLock.WhitelistPlayers != null) { foreach (var id in modularCar.CarLock.WhitelistPlayers) if (id == attacker.userID) return true; } return false; } var children = vehicle.children; if (children == null) return false; VehiclePrivilege vehiclePriv = null; for (int i = 0; i < children.Count; i++) { if (children[i] is VehiclePrivilege vp) { vehiclePriv = vp; break; } } if (vehiclePriv?.authorizedPlayers == null) return false; foreach (var entry in vehiclePriv.authorizedPlayers) if (AuthorizedEntryMatchesUserId(entry, attacker.userID)) return true; return false; } /// Om entity tel som base (BuildingBlock med twig-sjekk eller prefab på beskyttelsesliste). private bool IsBaseEntity(BaseCombatEntity entity) { if (entity == null) return false; if (entity is BuildingBlock block) return _config.ProtectTwig || block.grade != BuildingGrade.Enum.Twigs || IsProtectedPrefab(entity); return IsProtectedPrefab(entity); } /// Hentar kort prefab-navn (ShortPrefabName eller filnamn utan path/.prefab). private static string GetShortPrefabName(BaseCombatEntity entity) { if (entity == null) return null; string s = entity.ShortPrefabName; if (!string.IsNullOrEmpty(s)) return s; string full = entity.PrefabName; if (string.IsNullOrEmpty(full)) return null; int last = Math.Max(full.LastIndexOf('/'), full.LastIndexOf('\\')); s = last >= 0 ? full.Substring(last + 1) : full; if (s.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) s = s.Substring(0, s.Length - 7); return string.IsNullOrEmpty(s) ? null : s; } /// Om prefab-navnet (eller med _ -> .) står på beskyttelsesliste eller matcher delstreng. private bool IsProtectedPrefabByName(string name) { if (string.IsNullOrEmpty(name) || _protectedPrefabsSet == null) return false; if (IsNeverRaidBlockProtectedPrefabName(name)) return false; string n = name.Replace('_', '.'); return _protectedPrefabsSet.Contains(name) || _protectedPrefabsSet.Contains(n) || _protectedPrefabsSet.Any(p => name.IndexOf(p, StringComparison.OrdinalIgnoreCase) >= 0) || _protectedPrefabsSet.Any(p => n.IndexOf(p, StringComparison.OrdinalIgnoreCase) >= 0); } /// Om entity sin prefab e beskytta (bruker _prefabCache viss tilgjengeleg). private bool IsProtectedPrefab(BaseCombatEntity entity) { if (entity == null) return false; string nameEarly = GetShortPrefabName(entity); if (!string.IsNullOrEmpty(nameEarly) && IsNeverRaidBlockProtectedPrefabName(nameEarly)) { if (_prefabCache != null) _prefabCache[entity.prefabID] = false; return false; } uint id = entity.prefabID; if (_prefabCache != null && _prefabCache.TryGetValue(id, out bool cached)) return cached; string name = GetShortPrefabName(entity); if (string.IsNullOrEmpty(name)) { if (_prefabCache != null) _prefabCache[id] = false; return false; } bool result = IsProtectedPrefabByName(name); if (_prefabCache != null) _prefabCache[id] = result; return result; } /// Om angriparen har raidhours.bypass og kan raid uansett. private bool IsAttackerBypass(BasePlayer attacker) { if (attacker == null) return false; return permission.UserHasPermission(attacker.UserIDString, PermissionBypass); } /// Om TC (cupboard) har userId på authorizedPlayers. private static bool TcHasAuthorizedUserId(BuildingPrivlidge priv, ulong userId) { if (priv?.authorizedPlayers == null) return false; foreach (var entry in priv.authorizedPlayers) if (AuthorizedEntryMatchesUserId(entry, userId)) return true; return false; } /// Matchar auth-entry frå ulike API-typar (ulong eller objekt med userid/UserID). private static bool AuthorizedEntryMatchesUserId(object entry, ulong userId) { if (entry == null || userId == 0) return false; if (entry is ulong id) return id == userId; var t = entry.GetType(); var prop = t.GetProperty("userid", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) ?? t.GetProperty("userID", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (prop != null) { var val = prop.GetValue(entry, null); if (val is ulong pid) return pid == userId; } var field = t.GetField("userid", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) ?? t.GetField("userID", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (field != null) { var val = field.GetValue(entry); if (val is ulong fid) return fid == userId; } return false; } /// Om angriparen tel som base-eigar: TC-auth (UseTCAuthForOwnership) eller bygger/teammate med auth. private bool IsAttackerBaseOwner(BaseCombatEntity entity, BasePlayer attacker) { if (attacker == null) return false; ulong builderId = 0; if (_config.UseTCAuthForOwnership) { var priv = entity?.GetBuildingPrivilege(); if (priv == null || priv.IsDestroyed) return false; ulong entityNetId = entity.net?.ID.Value ?? 0UL; bool haveBuilder = entityNetId != 0 && _entityBuilderCache.TryGetValue(entityNetId, out builderId); if (haveBuilder) { if (!TcHasAuthorizedUserId(priv, builderId)) return false; } else { var cupboardEntity = (priv as Component)?.gameObject?.GetComponent(); if (cupboardEntity != null && cupboardEntity.net != null && _entityBuilderCache.TryGetValue(cupboardEntity.net.ID.Value, out ulong cupboardBuilderId) && cupboardBuilderId == attacker.userID) return true; } return priv.IsAuthed(attacker); } ulong netId = entity?.net?.ID.Value ?? 0UL; if (netId == 0 || !_entityBuilderCache.TryGetValue(netId, out builderId)) return false; if (attacker.userID == builderId) return true; if (attacker.currentTeam != 0) { var team = RelationshipManager.ServerInstance.FindTeam(attacker.currentTeam); if (team != null && team.members != null && team.members.Contains(builderId)) return true; } return false; } /// Hook: skade på BaseCombatEntity - prøv raid-block. private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) { return TryApplyRaidBlock(entity, info); } /// Hook: skade på DecayEntity - prøv raid-block. private object OnEntityTakeDamage(DecayEntity entity, HitInfo info) { return TryApplyRaidBlock(entity, info); } /// Hook: strukturangrep - prøv raid-block. private object OnStructureAttack(in BaseCombatEntity entity, ref HitInfo info) { return TryApplyRaidBlock(entity, info); } /// Hovudlogikk: viss entity e beskytta og no e raid-block, reduser eller null ut skade; sjekk bypass og eigar. private object TryApplyRaidBlock(BaseCombatEntity entity, HitInfo info) { if (entity == null || info == null) return null; if (info.damageTypes.Has(Rust.DamageType.Decay)) return null; string neverBlockName = GetShortPrefabName(entity); if (!string.IsNullOrEmpty(neverBlockName) && IsNeverRaidBlockProtectedPrefabName(neverBlockName)) return null; var localNow = GetCurrentTimeInZone(); int blockPercent; float blockPct; float scale; if (_config.ProtectVehicles && IsVehicleEntity(entity)) { // Never apply vehicle raid block to players (e.g. standing on boat = parented to boat, would block PvP damage) if (entity is BasePlayer) return null; var vehicle = GetRootVehicle(entity); string configName = vehicle != null ? GetVehicleConfigName(vehicle) : null; if (vehicle == null || !IsVehicleProtectedByConfig(entity, vehicle, configName)) return null; if (IsAttackerBypass(info.InitiatorPlayer)) return null; if (IsAttackerVehicleAuthorized(entity, info.InitiatorPlayer)) return null; if (!GetBlockPercentForCurrentWindow(localNow, out blockPercent)) return null; blockPct = Mathf.Clamp(blockPercent / 100f, 0f, 1f); if (blockPct >= 1f) { info.damageTypes = new Rust.DamageTypeList(); info.HitMaterial = 0; info.PointStart = info.PointEnd; NotifyAttackerRaidBlock(info.InitiatorPlayer, blockPercent); return true; } scale = 1f - blockPct; info.damageTypes.ScaleAll(scale); NotifyAttackerRaidBlock(info.InitiatorPlayer, blockPercent); return null; } if (!IsBaseEntity(entity)) return null; var priv = entity.GetBuildingPrivilege(); if (priv == null || priv.IsDestroyed) return null; if (priv.OwnerID == 0) return null; // TC with no owner = event/raid base if (IsAttackerBypass(info.InitiatorPlayer)) return null; if (IsAttackerBaseOwner(entity, info.InitiatorPlayer)) return null; if (!GetBlockPercentForCurrentWindow(localNow, out blockPercent)) return null; blockPct = Mathf.Clamp(blockPercent / 100f, 0f, 1f); if (blockPct >= 1f) { info.damageTypes = new Rust.DamageTypeList(); info.HitMaterial = 0; info.PointStart = info.PointEnd; NotifyAttackerRaidBlock(info.InitiatorPlayer, blockPercent); return true; } scale = 1f - blockPct; info.damageTypes.ScaleAll(scale); NotifyAttackerRaidBlock(info.InitiatorPlayer, blockPercent); return null; } /// Sender raid-melding til spelar (med chat-ikon viss ChatIconSteamId satt). private void SendRaidChat(BasePlayer player, string message) { if (player == null || string.IsNullOrEmpty(message)) return; if (!string.IsNullOrEmpty(_config.ChatIconSteamId) && ulong.TryParse(_config.ChatIconSteamId, out _)) { rust.SendChatMessage(player, string.Empty, message, _config.ChatIconSteamId); return; } SendReply(player, message); } /// Gir angriparen melding om at raid-block e aktiv (med cooldown). private void NotifyAttackerRaidBlock(BasePlayer attacker, int blockPercent) { if (attacker == null || _config.NotifyAttackerCooldownSeconds <= 0f) return; ulong uid = attacker.userID; var now = DateTime.UtcNow; if (_notifyCooldownUntil.TryGetValue(uid, out DateTime until) && now < until) return; if (_notifyCooldownUntil != null) { var expired = new List(); foreach (var kv in _notifyCooldownUntil) if (kv.Value < now) expired.Add(kv.Key); foreach (var key in expired) _notifyCooldownUntil.Remove(key); } _notifyCooldownUntil[uid] = now.AddSeconds(_config.NotifyAttackerCooldownSeconds); int pct = Mathf.Clamp(blockPercent, 0, 100); SendRaidChat(attacker, $"Raid block is active - your damage was reduced by {pct}%."); } private const string CommandHideGameTip = "gametip.hidegametip"; private const string CommandShowGameTipToast = "gametip.showtoast"; /// Sjekker om raid-block nettopp starta eller slutta; sender chat/hint til alle ved overgang. private void CheckRaidBlockTransition() { if (_config?.ProtectedTimeWindows == null || _config.ProtectedTimeWindows.Count == 0) return; var localNow = GetCurrentTimeInZone(); GetNextTransition(localNow, out bool inBlock, out TimeSpan untilChange); if (_lastBlockState.HasValue && _lastBlockState.Value != inBlock) { if (inBlock) { GetBlockPercentForCurrentWindow(localNow, out int pct); string durationStr = FormatTimeSpan(untilChange); if (_config.AnnounceBlockStart) BroadcastRaidMessage(_config.MessageBlockStarted.Replace("{pct}", pct.ToString()).Replace("{duration}", durationStr)); if (_config.ShowActiveHint && _config.HintDisplaySeconds > 0f) ShowRaidBlockHintToAll(untilChange, pct); } else { if (_config.AnnounceBlockEnd) BroadcastRaidMessage(_config.MessageBlockEnded); if (_config.ShowEndHint && _config.HintDisplaySeconds > 0f) ShowRaidBlockEndedHintToAll(); } } _lastBlockState = inBlock; } /// Sender raid-melding til alle tilkobla spelarar. private void BroadcastRaidMessage(string message) { if (string.IsNullOrEmpty(message)) return; foreach (var player in BasePlayer.activePlayerList) SendRaidChat(player, message); } /// Visar game hint til alle når raid-block startar (varighet og prosent). private void ShowRaidBlockHintToAll(TimeSpan duration, int pct) { string durationStr = FormatTimeSpan(duration); string text = (_config?.MessageHint ?? "Raid block active for {duration} with {pct}% protection") .Replace("{duration}", durationStr).Replace("{pct}", pct.ToString()); ShowHintToAll(text); } /// Visar game hint til alle når raid-block sluttar. private void ShowRaidBlockEndedHintToAll() { string text = _config?.MessageHintEnd ?? "Raid block has ended - raiding is allowed"; ShowHintToAll(text); } /// Sender game-tip til alle spelarar i HintDisplaySeconds sekund. private void ShowHintToAll(string text) { if (string.IsNullOrEmpty(text) || _config == null) return; float seconds = Mathf.Clamp(_config.HintDisplaySeconds, 1f, 60f); foreach (var player in BasePlayer.activePlayerList) { if (player == null || !player.IsConnected) continue; player.SendConsoleCommand(CommandHideGameTip); player.SendConsoleCommand(CommandShowGameTipToast, 2, text); float sec = seconds; timer.Once(sec, () => player?.SendConsoleCommand(CommandHideGameTip)); } } /// Lukkar game-tip for alle spelarar (ved unload). private void DestroyBlockHintForAll() { foreach (var player in BasePlayer.activePlayerList) player?.SendConsoleCommand(CommandHideGameTip); } /// Kommando /raid: viser noverande tid, tidssone, beskytta vinduer og kor lenge til block startar/sluttar. private void CmdRaid(BasePlayer player, string command, string[] args) { if (_config.RequirePermissionForRaidCommand && !permission.UserHasPermission(player.UserIDString, PermissionCheck) && !permission.UserHasPermission(player.UserIDString, PermissionAdmin)) { SendRaidChat(player, lang.GetMessage(Loc.NoPermission, this, player.UserIDString)); return; } var localNow = GetCurrentTimeInZone(); string timeStr = localNow.ToString("HH:mm"); string tzName = GetTimezoneDisplayLabelForUi(); GetNextTransition(localNow, out bool inBlock, out TimeSpan until); string dayLabel = localNow.ToString("dddd") + (IsWipeDayToday(localNow) ? " (WipeDay)" : ""); var lines = new List { lang.GetMessage(Loc.RaidHeader, this, player.UserIDString), lang.GetMessage(Loc.RaidCurrentTime, this, player.UserIDString).Replace("{time}", timeStr).Replace("{tz}", tzName).Replace("{day}", dayLabel), "" }; foreach (var w in _config.ProtectedTimeWindows) { if (w == null || !TryParseTime(w.Start, out _) || !TryParseTime(w.End, out _)) continue; lines.Add(lang.GetMessage(Loc.RaidProtectedHoursLine, this, player.UserIDString) .Replace("{start}", w.Start).Replace("{end}", w.End).Replace("{pct}", w.BlockPercent.ToString()).Replace("{day}", w.Day ?? "Everyday")); } lines.Add(""); if (inBlock) { string endsIn = FormatTimeSpan(until); lines.Add(lang.GetMessage(Loc.RaidBlockEndsIn, this, player.UserIDString).Replace("{duration}", endsIn)); } else { string startsIn = FormatTimeSpan(until); lines.Add(lang.GetMessage(Loc.RaidBlockStartsIn, this, player.UserIDString).Replace("{duration}", startsIn)); } SendRaidChat(player, string.Join("\n", lines)); } /// Formaterar TimeSpan til lesbar streng (t.d. "2h 30m" eller "< 1m"). private static string FormatTimeSpan(TimeSpan ts) { if (ts.TotalHours >= 1) return $"{(int)ts.TotalHours}h {ts.Minutes}m"; if (ts.TotalMinutes >= 1) return $"{(int)ts.TotalMinutes}m"; return "< 1m"; } /// Admin-kommando: opnar admin-panel eller /raidhours test <steamId> for raid-block test. private void CmdRaidHoursAdmin(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, PermissionAdmin)) { SendRaidChat(player, lang.GetMessage(Loc.NoPermission, this, player.UserIDString)); return; } if (args != null && args.Length >= 2 && string.Equals(args[0], "test", StringComparison.OrdinalIgnoreCase)) { CmdRaidHoursTest(player, args[1]); return; } OpenAdminPanel(player); } /// Test-kommando: finn spelar på steamId, raycast mot entity, kjør RunRaidBlockTest og vis resultat. private void CmdRaidHoursTest(BasePlayer admin, string steamIdArg) { if (admin == null) return; if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { SendReply(admin, "[RaidHours test] Invalid SteamID."); return; } BasePlayer target = BasePlayer.FindByID(targetSteamId) ?? BasePlayer.FindSleeping(targetSteamId); if (target == null) { SendReply(admin, $"[RaidHours test] No player found for {targetSteamId}."); return; } RaycastHit hit; if (!Physics.Raycast(admin.eyes.HeadRay(), out hit, 10f)) { SendReply(admin, "[RaidHours test] Look at an entity and run again."); return; } var hitEntity = hit.GetEntity(); if (hitEntity == null) { SendReply(admin, "[RaidHours test] No entity under crosshair."); return; } var entity = hitEntity as BaseCombatEntity; if (entity == null) { SendReply(admin, $"[RaidHours test] Entity is not BaseCombatEntity: {hitEntity.GetType().Name}"); return; } var lines = RunRaidBlockTest(entity, target); foreach (var line in lines) SendReply(admin, line); } /// Kjører raid-block logikk for entity og angripar; returnerer liste med debug-linjer for admin. private List RunRaidBlockTest(BaseCombatEntity entity, BasePlayer attacker) { var lines = new List(); var localNow = GetCurrentTimeInZone(); int blockPercent = 0; bool inWindow = GetBlockPercentForCurrentWindow(localNow, out blockPercent); lines.Add($"[RaidHours test] Entity: {entity.GetType().Name} | Prefab: {GetShortPrefabName(entity) ?? "?"}"); lines.Add($" InRaidWindow: {inWindow} | BlockPct: {blockPercent} | Attacker: {attacker?.displayName ?? "?"} ({attacker?.userID ?? 0})"); bool isVehicle = IsVehicleEntity(entity); lines.Add($" IsVehicleEntity: {isVehicle}"); if (isVehicle && _config.ProtectVehicles) { var vehicle = GetRootVehicle(entity); string configName = vehicle != null ? GetVehicleConfigName(vehicle) : null; bool protectedByConfig = vehicle != null && IsVehicleProtectedByConfig(entity, vehicle, configName); bool bypass = IsAttackerBypass(attacker); bool vehicleAuth = IsAttackerVehicleAuthorized(entity, attacker); lines.Add($" ProtectVehicles: true | Vehicle: {vehicle?.GetType().Name ?? "null"} | ConfigName: {configName ?? "?"}"); lines.Add($" VehicleProtectedByConfig: {protectedByConfig} | AttackerBypass: {bypass} | AttackerVehicleAuthorized: {vehicleAuth}"); bool wouldBlock = protectedByConfig && !bypass && !vehicleAuth && inWindow; lines.Add($" => Would raid block: {wouldBlock}"); return lines; } if (isVehicle) { lines.Add($" => Not checking vehicle (ProtectVehicles off or not in config). Would block: false"); return lines; } bool isBase = IsBaseEntity(entity); lines.Add($" IsBaseEntity: {isBase}"); if (!isBase) { lines.Add($" => Not a protected base entity. Would block: false"); return lines; } var priv = entity.GetBuildingPrivilege(); bool privOk = priv != null && !priv.IsDestroyed; bool bypassBase = IsAttackerBypass(attacker); bool baseOwner = IsAttackerBaseOwner(entity, attacker); lines.Add($" BuildingPrivilege: {(privOk ? "ok" : "null/destroyed")} | AttackerBypass: {bypassBase} | AttackerBaseOwner: {baseOwner}"); bool wouldBlockBase = privOk && !bypassBase && !baseOwner && inWindow; lines.Add($" => Would raid block: {wouldBlockBase}"); return lines; } private const string ConsoleCommandPrefix = "raidhours_ui"; private const string UiBlurBg = "0 0 0 0.6"; private const string UiMainBg = "0 0 0 0.6"; private const string UiSectionBg = "0 0 0 0.7"; private const string UiRowBg = "0 0 0 0.5"; private const string UiRowBgAlt = "0 0 0 0.4"; private const string UiStatusBarBg = "0.1 0.1 0.12 0.95"; private const string UiSeparator = "0.3 0.3 0.35 0.9"; private const string UiPrimaryBtn = "0.25 0.45 0.25 0.9"; private const string UiDangerBtn = "0.5 0.15 0.15 0.9"; private const string UiSecondaryBtn = "1 1 1 0.15"; private const string UiText = "1 1 1 1"; private const string UiTextMuted = "0.85 0.85 0.88 1"; private const string UiFont = "robotocondensed-bold.ttf"; private const float OptionRowGap = 0.02f; private const float OptionsPanelMaxX = 0.40f; private const float WindowsPanelMinX = 0.43f; /// Lag ein CUI-panel og legg han i container; valgfritt med blur. private static string CreatePanel(ref CuiElementContainer container, string anchorMin, string anchorMax, string panelColor, string parent, string panelName = null, bool isMainPanel = false, bool blur = false) { var panel = new CuiPanel { RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }, Image = { Color = panelColor, FadeIn = 0f }, CursorEnabled = isMainPanel }; if (blur) panel.Image.Material = "assets/content/ui/uibackgroundblur.mat"; return container.Add(panel, parent, panelName); } /// Lag eit CUI-label (tekst) i container. private static void CreateLabel(ref CuiElementContainer container, string anchorMin, string anchorMax, string textColor, string backgroundColor, string text, int fontSize, TextAnchor alignment, string parent) { string panel = CreatePanel(ref container, anchorMin, anchorMax, backgroundColor, parent); container.Add(new CuiLabel { Text = { Color = textColor, Text = text, Align = alignment, FontSize = fontSize, Font = UiFont }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, panel); } /// Lag ein CUI-knapp med kommando. private static void CreateButton(ref CuiElementContainer container, string anchorMin, string anchorMax, string buttonColor, string textColor, string buttonText, int fontSize, string command, string parent, TextAnchor labelAnchor = TextAnchor.MiddleCenter) { container.Add(new CuiButton { Button = { Color = buttonColor, Command = command }, RectTransform = { AnchorMin = anchorMin, AnchorMax = anchorMax }, Text = { Align = labelAnchor, Color = textColor, FontSize = fontSize, Text = buttonText, Font = UiFont } }, parent); } /// Gir lesbar UTC-offset for tidssone (t.d. "UTC+1" eller "UTC-5"). private static string GetUtcOffsetDisplayName(TimeZoneInfo tz) { int totalMinutes = (int)tz.GetUtcOffset(DateTime.UtcNow).TotalMinutes; if (totalMinutes == 0) return "UTC"; int h = totalMinutes / 60; int m = Math.Abs(totalMinutes % 60); if (totalMinutes < 0) return "UTC" + h + (m != 0 ? $":{m:D2}" : ""); return "UTC+" + h + (m != 0 ? $":{m:D2}" : ""); } /// Formaterar offset i timar til streng (t.d. UTC+1). private static string FormatUtcOffsetFromHours(double offsetHours) { if (Math.Abs(offsetHours) < 0.001) return "UTC"; int totalMinutes = (int)Math.Round(offsetHours * 60); int h = totalMinutes / 60; int m = Math.Abs(totalMinutes % 60); if (totalMinutes < 0) return "UTC" + h + (m != 0 ? $":{m:D2}" : ""); return "UTC+" + h + (m != 0 ? $":{m:D2}" : ""); } /// Tidssone-tekst for chat og admin-UI: brukar faktisk offset (DST) når TimeZoneInfo er lasta. private string GetTimezoneDisplayLabelForUi() { int tzIndex = Array.IndexOf(CommonTimeZoneIds, _config.TimezoneId); if (tzIndex >= 0 && tzIndex < CommonTimeZoneIds.Length) { if (string.Equals(_config.TimezoneId, "UTC", StringComparison.OrdinalIgnoreCase)) return "UTC (UTC)"; if (_timeZone != null) return GetUtcOffsetDisplayName(_timeZone) + " " + _config.TimezoneId; if (tzIndex < CommonTimeZoneDisplayNames.Length) return CommonTimeZoneDisplayNames[tzIndex]; } if (_timeZone != null && !string.IsNullOrWhiteSpace(_config.TimezoneId)) return GetUtcOffsetDisplayName(_timeZone) + " " + _config.TimezoneId; return FormatUtcOffsetFromHours(_fallbackOffsetHours ?? 0); } /// Opnar admin-panelet for spelaren (overlay, status, options, vinduer). Valgfritt scroll til rad. private void OpenAdminPanel(BasePlayer player, int? scrollToRow = null, bool fromUiAction = false) { _adminOpenFromUiAction = fromUiAction; if (scrollToRow.HasValue) _adminScrollToRow[player.userID] = scrollToRow.Value; CloseAllRaidHoursUi(player); var container = new CuiElementContainer(); string overlay = CreatePanel(ref container, "0 0", "1 1", UiBlurBg, "Overlay", MainPanelName + "_overlay", isMainPanel: true, blur: true); CreateButton(ref container, "0 0", "1 1", "0 0 0 0", "0 0 0 0", "", 1, $"{ConsoleCommandPrefix} close", overlay, TextAnchor.MiddleCenter); string main = CreatePanel(ref container, "0.12 0.12", "0.88 0.9", UiMainBg, overlay, MainPanelName, isMainPanel: true); CreateLabel(ref container, "0 0.92", "1 0.98", UiText, "0 0 0 0", "Raid Hours", 24, TextAnchor.MiddleCenter, main); var localNow = GetCurrentTimeInZone(); GetNextTransition(localNow, out bool inBlock, out TimeSpan untilChange); string statusText = inBlock ? $"Raid block: ACTIVE - ends in {FormatTimeSpan(untilChange)}" : $"Raid block: OFF - starts in {FormatTimeSpan(untilChange)}"; string statusColor = inBlock ? "0.2 0.6 0.25 1" : "0.7 0.65 0.2 1"; string statusPanel = CreatePanel(ref container, "0.02 0.86", "0.98 0.91", "0 0 0 0", main, MainPanelName + "_status"); CreatePanel(ref container, "0 0", "1 1", UiStatusBarBg, statusPanel); CreateLabel(ref container, "0 0", "1 1", statusColor, "0 0 0 0", statusText, 13, TextAnchor.MiddleCenter, statusPanel); CreateButton(ref container, "0.88 0.92", "0.98 0.98", UiDangerBtn, UiText, "Close", 14, $"{ConsoleCommandPrefix} close", main, TextAnchor.MiddleCenter); float y = 0.84f; BuildOptionsSection(ref container, main, localNow); BuildWindowsSection(ref container, main, player, y); var playerRef = player; timer.Once(0f, () => { if (_config == null) return; timer.Once(0f, () => { if (_config == null) return; if (playerRef != null && playerRef.IsConnected) { CuiHelper.AddUi(playerRef, container); _adminPanelOpenForPlayers.Add(playerRef.userID); if (!_adminOpenFromUiAction) { timer.Once(0f, () => { if (_config == null) return; if (playerRef != null && playerRef.IsConnected) { CloseAllRaidHoursUi(playerRef); OpenAdminPanel(playerRef, null, fromUiAction: true); } }); } } }); }); } /// Oppdaterar berre status-linja (raid block on/off) for alle som har panelet opent. private void RefreshAdminPanelStatusForAll() { if (_config == null || _adminPanelOpenForPlayers == null || _adminPanelOpenForPlayers.Count == 0) return; foreach (var uid in _adminPanelOpenForPlayers.ToList()) { var p = BasePlayer.FindByID(uid); if (p != null && p.IsConnected) RefreshAdminPanelStatus(p); } } /// Oppdaterar berre status-panelet for ein spelar (raid block ACTIVE/OFF og countdown). private void RefreshAdminPanelStatus(BasePlayer player) { if (player == null || !player.IsConnected || _config == null) return; CuiHelper.DestroyUi(player, MainPanelName + "_status"); var container = new CuiElementContainer(); var localNow = GetCurrentTimeInZone(); GetNextTransition(localNow, out bool inBlock, out TimeSpan untilChange); string statusText = inBlock ? $"Raid block: ACTIVE - ends in {FormatTimeSpan(untilChange)}" : $"Raid block: OFF - starts in {FormatTimeSpan(untilChange)}"; string statusColor = inBlock ? "0.2 0.6 0.25 1" : "0.7 0.65 0.2 1"; string statusPanel = CreatePanel(ref container, "0.02 0.86", "0.98 0.91", "0 0 0 0", MainPanelName, MainPanelName + "_status"); CreatePanel(ref container, "0 0", "1 1", UiStatusBarBg, statusPanel); CreateLabel(ref container, "0 0", "1 1", statusColor, "0 0 0 0", statusText, 13, TextAnchor.MiddleCenter, statusPanel); CuiHelper.AddUi(player, container); } /// Bygger options-delen av admin-panelet (tidssone, toggles for turrets/twig/vehicles/TC). private void BuildOptionsSection(ref CuiElementContainer container, string mainParent, DateTime localNow) { const float y = 0.84f; string sectionOpt = CreatePanel(ref container, "0.05 0.04", $"{OptionsPanelMaxX} {y}", UiSectionBg, mainParent, MainPanelName + "_secopt"); CreateLabel(ref container, "0.05 0.92", "0.95 0.98", UiTextMuted, "0 0 0 0", "OPTIONS", 12, TextAnchor.MiddleCenter, sectionOpt); CreatePanel(ref container, "0.05 0.88", "0.95 0.90", UiSeparator, sectionOpt); float oy = 0.88f; string tzDisplay = GetTimezoneDisplayLabelForUi(); CreateLabel(ref container, "0.05 0.82", "0.95 0.86", UiTextMuted, "0 0 0 0", "Timezone", 10, TextAnchor.MiddleLeft, sectionOpt); CreateLabel(ref container, "0.05 0.76", "0.6 0.82", UiText, "0 0 0 0", $"{tzDisplay} {localNow:HH:mm}", 13, TextAnchor.MiddleLeft, sectionOpt); CreateButton(ref container, "0.62 0.76", "0.95 0.82", UiPrimaryBtn, UiText, "Change", 12, $"{ConsoleCommandPrefix} timezone", sectionOpt); oy = 0.74f; float oRow = 0.1f; CreateToggleRow(ref container, sectionOpt, ref oy, oRow, "Protect turrets, SAM & traps", _config.ProtectTurretsAndTraps, $"{ConsoleCommandPrefix} toggleturretsandtraps"); CreateToggleRow(ref container, sectionOpt, ref oy, oRow, "Protect all deployables", _config.ProtectAllDeployables, $"{ConsoleCommandPrefix} togglealldeployables"); CreateToggleRow(ref container, sectionOpt, ref oy, oRow, "Protect twig building blocks", _config.ProtectTwig, $"{ConsoleCommandPrefix} toggletwig"); CreateToggleRow(ref container, sectionOpt, ref oy, oRow, "Protect vehicles (Modular boats & Tugboat)", _config.ProtectVehicles, $"{ConsoleCommandPrefix} togglevehicles"); CreateToggleRow(ref container, sectionOpt, ref oy, oRow, "Base owner = TC authed (on) or builder/teammate & auth (off)", _config.UseTCAuthForOwnership, $"{ConsoleCommandPrefix} toggletcauth"); CreateLabel(ref container, "0.05 0.02", "0.95 0.065", UiTextMuted, "0 0 0 0", "Changes save automatically.", 10, TextAnchor.MiddleLeft, sectionOpt); } /// Bygger vinduer-delen: liste over tidsvinduer med start/slutt/block%/dag og knapp for å legg til. private void BuildWindowsSection(ref CuiElementContainer container, string mainParent, BasePlayer player, float y) { string sectionWin = CreatePanel(ref container, $"{WindowsPanelMinX} 0.04", $"0.95 {y}", UiSectionBg, mainParent, MainPanelName + "_secwin"); CreateLabel(ref container, "0.05 0.92", "0.95 0.98", UiTextMuted, "0 0 0 0", "PROTECTED WINDOWS (24h format)", 12, TextAnchor.MiddleCenter, sectionWin); CreatePanel(ref container, "0.05 0.88", "0.95 0.90", UiSeparator, sectionWin); CreateLabel(ref container, "0.03 0.84", "0.18 0.88", UiTextMuted, "0 0 0 0", "Start", 10, TextAnchor.MiddleCenter, sectionWin); CreateLabel(ref container, "0.20 0.84", "0.36 0.88", UiTextMuted, "0 0 0 0", "End", 10, TextAnchor.MiddleCenter, sectionWin); CreateLabel(ref container, "0.38 0.84", "0.52 0.88", UiTextMuted, "0 0 0 0", "Block %", 10, TextAnchor.MiddleCenter, sectionWin); CreateLabel(ref container, "0.54 0.84", "0.72 0.88", UiTextMuted, "0 0 0 0", "Start Day", 10, TextAnchor.MiddleCenter, sectionWin); const int winRowHeightPx = 32; const int winRowGapPx = 4; const int winAddButtonHeightPx = 36; const int winAddButtonGapPx = 8; int winCount = _config.ProtectedTimeWindows.Count; int scrollContentHeightPx = winCount * (winRowHeightPx + winRowGapPx) + winAddButtonGapPx + winAddButtonHeightPx; int scrollRow = _adminScrollToRow.TryGetValue(player.userID, out int stored) ? stored : 0; _adminScrollToRow.Remove(player.userID); scrollRow = Mathf.Clamp(scrollRow, 0, Mathf.Max(0, winCount)); int scrollOffsetPx = scrollRow * (winRowHeightPx + winRowGapPx); const string winScrollContainerName = MainPanelName + "_win_scroll"; CreatePanel(ref container, "0.03 0.04", "0.97 0.80", "0 0 0 0", sectionWin, winScrollContainerName); const string winScrollViewName = MainPanelName + "_win_scrollview"; int contentOffsetMinY = -scrollContentHeightPx + scrollOffsetPx; int contentOffsetMaxY = scrollOffsetPx; container.Add(new CuiElement { Name = winScrollViewName, Parent = winScrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = "0 0 0 0" }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = ScrollRect.MovementType.Elastic, Elasticity = 0.1f, Inertia = true, DecelerationRate = 0.135f, ScrollSensitivity = 20f, ContentTransform = new CuiRectTransform { AnchorMin = "0 0.98", AnchorMax = "1 0.98", OffsetMin = $"0 {contentOffsetMinY}", OffsetMax = $"0 {contentOffsetMaxY}", Pivot = "0.5 1" } }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", Pivot = "0.5 1" } } }); for (int i = 0; i < winCount; i++) { var w = _config.ProtectedTimeWindows[i]; int idx = i; int offsetFromTop = i * (winRowHeightPx + winRowGapPx); int rowOffsetMin = -offsetFromTop - winRowHeightPx; int rowOffsetMax = -offsetFromTop; string rowColor = (i % 2 == 0) ? UiRowBg : UiRowBgAlt; string rowName = MainPanelName + "_win_row_" + idx; container.Add(new CuiElement { Name = rowName, Parent = winScrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0 0.998", AnchorMax = "1 0.998", OffsetMin = $"0 {rowOffsetMin}", OffsetMax = $"0 {rowOffsetMax}" }, new CuiImageComponent { Color = rowColor } } }); CreateWindowInput(ref container, rowName, "0.02 0.08", "0.18 0.92", w.Start, 11, $"{ConsoleCommandPrefix} setstart {idx} ", MainPanelName + "_win_start_" + idx); CreateWindowInput(ref container, rowName, "0.20 0.08", "0.36 0.92", w.End, 11, $"{ConsoleCommandPrefix} setend {idx} ", MainPanelName + "_win_end_" + idx); CreateWindowInput(ref container, rowName, "0.38 0.08", "0.52 0.92", w.BlockPercent.ToString(), 11, $"{ConsoleCommandPrefix} setblockpct {idx} ", MainPanelName + "_win_pct_" + idx); string dayLabel = GetDayDisplayLabel(w.Day); string dayBtnParent = MainPanelName + "_day_btn_" + idx; CreatePanel(ref container, "0.54 0.08", "0.72 0.92", UiSecondaryBtn, rowName, dayBtnParent); CreateButton(ref container, "0 0", "1 1", "0 0 0 0", UiText, dayLabel, 10, $"{ConsoleCommandPrefix} day {idx}", dayBtnParent, TextAnchor.MiddleCenter); CreateButton(ref container, "0.74 0.08", "0.97 0.92", UiDangerBtn, UiText, "Remove", 11, $"{ConsoleCommandPrefix} delwindow {idx}", rowName, TextAnchor.MiddleCenter); } int addBtnOffsetFromTop = winCount * (winRowHeightPx + winRowGapPx) + winAddButtonGapPx; int addBtnOffsetMin = -addBtnOffsetFromTop - winAddButtonHeightPx; string addBtnRowName = MainPanelName + "_win_addrow"; container.Add(new CuiElement { Name = addBtnRowName, Parent = winScrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0 0.998", AnchorMax = "1 0.998", OffsetMin = $"0 {addBtnOffsetMin}", OffsetMax = $"0 {-addBtnOffsetFromTop}" }, new CuiImageComponent { Color = "0 0 0 0" } } }); CreateButton(ref container, "0.05 0.1", "0.95 0.9", UiPrimaryBtn, UiText, "+ Add time window", 12, $"{ConsoleCommandPrefix} addwindow", addBtnRowName, TextAnchor.MiddleCenter); } /// Oppdaterar options-delen i admin-panelet for spelaren. private void RefreshOptionsSection(BasePlayer player) { CuiHelper.DestroyUi(player, MainPanelName + "_secopt"); var container = new CuiElementContainer(); BuildOptionsSection(ref container, MainPanelName, GetCurrentTimeInZone()); CuiHelper.AddUi(player, container); } /// Oppdaterar vinduer-delen i admin-panelet; valgfritt scroll til bestemt rad. private void RefreshWindowsSection(BasePlayer player, int? scrollToRow = null) { if (scrollToRow.HasValue) _adminScrollToRow[player.userID] = scrollToRow.Value; CuiHelper.DestroyUi(player, MainPanelName + "_secwin"); var container = new CuiElementContainer(); BuildWindowsSection(ref container, MainPanelName, player, 0.84f); CuiHelper.AddUi(player, container); } /// Lag ein rad med label og On/Off-knapp (toggle). private static void CreateToggleRow(ref CuiElementContainer container, string parent, ref float y, float rowH, string label, bool on, string command) { y -= rowH; string rowName = CreatePanel(ref container, $"0.05 {y - rowH}", $"0.95 {y}", UiRowBg, parent); CreateLabel(ref container, "0.05 0.1", "0.7 0.9", UiText, "0 0 0 0", label, 12, TextAnchor.MiddleLeft, rowName); string btnColor = on ? UiPrimaryBtn : UiSecondaryBtn; string btnText = on ? "On" : "Off"; CreateButton(ref container, "0.72 0.1", "0.95 0.9", btnColor, UiText, btnText, 12, command, rowName); y -= OptionRowGap; } /// Gir visningslabel for dag (Everyday el. ukedag/WipeDay). private static string GetDayDisplayLabel(string day) { if (string.IsNullOrWhiteSpace(day)) return "Everyday"; if (string.Equals(day, "Everyday", StringComparison.OrdinalIgnoreCase)) return "Everyday"; foreach (string opt in DayOptions) if (string.Equals(opt, day, StringComparison.OrdinalIgnoreCase)) return opt; return day; } /// Lag eit input-felt for vindue (start/slutt/block%) som sender kommando ved endring. private static void CreateWindowInput(ref CuiElementContainer container, string parent, string anchorMin, string anchorMax, string text, int fontSize, string command, string panelName) { string panel = CreatePanel(ref container, anchorMin, anchorMax, UiRowBg, parent, panelName); container.Add(new CuiElement { Parent = panel, Components = { new CuiInputFieldComponent { Color = UiText, Text = text, Align = TextAnchor.MiddleCenter, FontSize = fontSize, Font = UiFont, NeedsKeyboard = true, Command = command, CharsLimit = 10 }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" } } }); } /// Opnar dropdown for å velge tidssone (scroll-liste med CommonTimeZoneIds). private void OpenTimezoneDropdown(BasePlayer player) { CuiHelper.DestroyUi(player, MainPanelName + "_tz"); var container = new CuiElementContainer(); const string UiDropdownOpaque = "0.12 0.12 0.14 1"; const string UiDropdownScrollBg = "0.16 0.16 0.18 1"; string panel = CreatePanel(ref container, "0.59 0.12", "0.905 0.70", UiDropdownOpaque, MainPanelName, MainPanelName + "_tz"); CreateLabel(ref container, "0.05 0.92", "0.95 0.98", UiTextMuted, "0 0 0 0", "SELECT TIMEZONE", 12, TextAnchor.MiddleCenter, panel); const string scrollContainerName = MainPanelName + "_tz_scroll"; string scrollContainer = CreatePanel(ref container, "0.03 0.06", "0.97 0.88", UiDropdownScrollBg, panel, scrollContainerName); int count = CommonTimeZoneIds.Length; int rowHeightPx = 26; int rowGapPx = 4; int panelSize = -count * (rowHeightPx + rowGapPx); const string scrollViewName = MainPanelName + "_tz_scrollview"; container.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = UiDropdownScrollBg }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = ScrollRect.MovementType.Elastic, Elasticity = 0.1f, Inertia = true, DecelerationRate = 0.135f, ScrollSensitivity = 20f, ContentTransform = new CuiRectTransform { AnchorMin = "0 0.98", AnchorMax = "1 0.98", OffsetMin = $"0 {panelSize}", OffsetMax = "0 0", Pivot = "0.5 1" } }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", Pivot = "0.5 1" } } }); for (int i = 0; i < count; i++) { int index = i; int offsetFromTop = i * (rowHeightPx + rowGapPx); int offsetMin = -offsetFromTop - rowHeightPx; int offsetMax = -offsetFromTop; string rowColor = (i % 2 == 0) ? "0.2 0.2 0.22 1" : "0.18 0.18 0.2 1"; string rowName = MainPanelName + "_tz_row_" + index; container.Add(new CuiElement { Name = rowName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0.02 0.998", AnchorMax = "0.98 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = rowColor } } }); string tzId = CommonTimeZoneIds[i]; double offsetHours = i < CommonTimeZoneOffsetHours.Length ? CommonTimeZoneOffsetHours[i] : 0; string displayName = i < CommonTimeZoneDisplayNames.Length ? CommonTimeZoneDisplayNames[i] : FormatUtcOffsetFromHours(offsetHours); string rowTime = "-"; if (TryResolveTimeZoneForCommonId(tzId, out TimeZoneInfo tzRow)) { rowTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzRow).ToString("HH:mm"); if (string.Equals(tzId, "UTC", StringComparison.OrdinalIgnoreCase)) displayName = "UTC (UTC)"; else displayName = GetUtcOffsetDisplayName(tzRow) + " " + tzId; } else rowTime = DateTime.UtcNow.AddHours(offsetHours).ToString("HH:mm"); CreateLabel(ref container, "0.03 0.05", "0.72 0.95", UiText, "0 0 0 0", displayName, 12, TextAnchor.MiddleLeft, rowName); CreateLabel(ref container, "0.74 0.05", "0.97 0.95", UiText, "0 0 0 0", rowTime, 12, TextAnchor.MiddleRight, rowName); CreateButton(ref container, "0 0", "1 1", "0 0 0 0", "0 0 0 0", "", 12, $"{ConsoleCommandPrefix} settz {index}", rowName, TextAnchor.MiddleLeft); } CreateButton(ref container, "0.1 0.01", "0.9 0.045", UiSecondaryBtn, UiText, "Cancel", 12, $"{ConsoleCommandPrefix} closetz", panel); CuiHelper.AddUi(player, container); } /// Opnar dropdown for å velge start-dag for eit tidsvindue (Everyday, WipeDay, ukedagar). private void OpenDayDropdown(BasePlayer player, int windowIdx) { if (windowIdx < 0 || windowIdx >= _config.ProtectedTimeWindows.Count) return; string dayPanelName = MainPanelName + "_day"; CuiHelper.DestroyUi(player, dayPanelName); string mainParent = MainPanelName; var container = new CuiElementContainer(); const string UiDropdownOpaque = "0.12 0.12 0.14 1"; const string UiDropdownScrollBg = "0.16 0.16 0.18 1"; int rowHeightPx = 26; int rowGapPx = 4; int contentHeight = DayOptions.Length * (rowHeightPx + rowGapPx); string panel = CreatePanel(ref container, "0.48 0.15", "0.62 0.75", UiDropdownOpaque, mainParent, dayPanelName); CreateLabel(ref container, "0.05 0.92", "0.95 0.98", UiTextMuted, "0 0 0 0", "SELECT START DAY", 12, TextAnchor.MiddleCenter, panel); const string scrollContainerName = MainPanelName + "_day_scroll"; string scrollContainer = CreatePanel(ref container, "0.03 0.08", "0.97 0.88", UiDropdownScrollBg, panel, scrollContainerName); int panelSize = -contentHeight; const string scrollViewName = MainPanelName + "_day_scrollview"; container.Add(new CuiElement { Name = scrollViewName, Parent = scrollContainerName, Components = { new CuiNeedsCursorComponent(), new CuiImageComponent { Color = UiDropdownScrollBg }, new CuiScrollViewComponent { Horizontal = false, Vertical = true, MovementType = ScrollRect.MovementType.Elastic, Elasticity = 0.1f, Inertia = true, DecelerationRate = 0.135f, ScrollSensitivity = 20f, ContentTransform = new CuiRectTransform { AnchorMin = "0 0.98", AnchorMax = "1 0.98", OffsetMin = $"0 {panelSize}", OffsetMax = "0 0", Pivot = "0.5 1" } }, new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", Pivot = "0.5 1" } } }); for (int i = 0; i < DayOptions.Length; i++) { int dayIndex = i; int offsetFromTop = i * (rowHeightPx + rowGapPx); int offsetMin = -offsetFromTop - rowHeightPx; int offsetMax = -offsetFromTop; string rowColor = (i % 2 == 0) ? "0.2 0.2 0.22 1" : "0.18 0.18 0.2 1"; string rowName = MainPanelName + "_day_row_" + dayIndex; container.Add(new CuiElement { Name = rowName, Parent = scrollViewName, Components = { new CuiRectTransformComponent { AnchorMin = "0.02 0.998", AnchorMax = "0.98 0.998", OffsetMin = $"0 {offsetMin}", OffsetMax = $"0 {offsetMax}" }, new CuiImageComponent { Color = rowColor } } }); CreateButton(ref container, "0 0", "1 1", "0 0 0 0", UiText, DayOptions[dayIndex], 12, $"{ConsoleCommandPrefix} setday {windowIdx} {dayIndex}", rowName, TextAnchor.MiddleCenter); } CreateButton(ref container, "0.1 0.01", "0.9 0.045", UiSecondaryBtn, UiText, "Cancel", 12, $"{ConsoleCommandPrefix} closeday", panel); CuiHelper.AddUi(player, container); } /// Console-kommandoar frå admin-UI: close, timezone, setday, setstart, setend, toggles, add/del window osv. [ConsoleCommand(ConsoleCommandPrefix)] private void CC_RaidHoursUi(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null || !arg.HasArgs()) return; if (!permission.UserHasPermission(player.UserIDString, PermissionAdmin)) return; string cmd = arg.Args[0].ToLowerInvariant(); if (cmd == "close") { CloseAllRaidHoursUi(player); _adminScrollToRow?.Remove(player.userID); _adminPanelOpenForPlayers?.Remove(player.userID); return; } if (cmd == "timezone") { OpenTimezoneDropdown(player); return; } if (cmd == "closetz") { CuiHelper.DestroyUi(player, MainPanelName + "_tz_scrollview"); CuiHelper.DestroyUi(player, MainPanelName + "_tz_scroll"); CuiHelper.DestroyUi(player, MainPanelName + "_tz"); RefreshOptionsSection(player); return; } if (cmd == "closeday") { CuiHelper.DestroyUi(player, MainPanelName + "_day_scrollview"); CuiHelper.DestroyUi(player, MainPanelName + "_day_scroll"); CuiHelper.DestroyUi(player, MainPanelName + "_day"); RefreshWindowsSection(player); return; } if (cmd == "day" && arg.Args.Length >= 2 && int.TryParse(arg.Args[1], out int dayWindowIdx) && dayWindowIdx >= 0 && dayWindowIdx < _config.ProtectedTimeWindows.Count) { OpenDayDropdown(player, dayWindowIdx); return; } if (cmd == "setday" && arg.Args.Length >= 3 && int.TryParse(arg.Args[1], out int setDayWindowIdx) && setDayWindowIdx >= 0 && setDayWindowIdx < _config.ProtectedTimeWindows.Count && int.TryParse(arg.Args[2], out int dayOptIdx) && dayOptIdx >= 0 && dayOptIdx < DayOptions.Length) { _config.ProtectedTimeWindows[setDayWindowIdx].Day = DayOptions[dayOptIdx]; SaveConfig(); ApplyConfig(); CuiHelper.DestroyUi(player, MainPanelName + "_day_scrollview"); CuiHelper.DestroyUi(player, MainPanelName + "_day_scroll"); CuiHelper.DestroyUi(player, MainPanelName + "_day"); RefreshWindowsSection(player, setDayWindowIdx); return; } if (cmd == "settz" && arg.Args.Length >= 2 && int.TryParse(arg.Args[1], out int tzIndex)) { if (tzIndex >= 0 && tzIndex < CommonTimeZoneIds.Length) { _config.TimezoneId = CommonTimeZoneIds[tzIndex]; SaveConfig(); ApplyConfig(); } CuiHelper.DestroyUi(player, MainPanelName + "_tz_scrollview"); CuiHelper.DestroyUi(player, MainPanelName + "_tz_scroll"); CuiHelper.DestroyUi(player, MainPanelName + "_tz"); RefreshOptionsSection(player); return; } if (cmd == "toggleturretsandtraps") { _config.ProtectTurretsAndTraps = !_config.ProtectTurretsAndTraps; SaveConfig(); ApplyConfig(); RefreshOptionsSection(player); return; } if (cmd == "togglealldeployables") { _config.ProtectAllDeployables = !_config.ProtectAllDeployables; SaveConfig(); ApplyConfig(); RefreshOptionsSection(player); return; } if (cmd == "toggletwig") { _config.ProtectTwig = !_config.ProtectTwig; SaveConfig(); ApplyConfig(); RefreshOptionsSection(player); return; } if (cmd == "togglevehicles") { _config.ProtectVehicles = !_config.ProtectVehicles; SaveConfig(); ApplyConfig(); RefreshOptionsSection(player); return; } if (cmd == "toggletcauth") { _config.UseTCAuthForOwnership = !_config.UseTCAuthForOwnership; SaveConfig(); ApplyConfig(); RefreshOptionsSection(player); return; } if (cmd == "setstart" && arg.Args.Length >= 3 && int.TryParse(arg.Args[1], out int setStartIdx) && setStartIdx >= 0 && setStartIdx < _config.ProtectedTimeWindows.Count) { string val = NormalizeTimeInput(arg.Args[2]?.Trim() ?? ""); if (TryParseTime(val, out int tm)) { _config.ProtectedTimeWindows[setStartIdx].Start = $"{tm / 60:D2}:{tm % 60:D2}"; SaveConfig(); ApplyConfig(); } RefreshWindowsSection(player, setStartIdx); return; } if (cmd == "setend" && arg.Args.Length >= 3 && int.TryParse(arg.Args[1], out int setEndIdx) && setEndIdx >= 0 && setEndIdx < _config.ProtectedTimeWindows.Count) { string val = NormalizeTimeInput(arg.Args[2]?.Trim() ?? ""); if (TryParseTime(val, out int tm)) { _config.ProtectedTimeWindows[setEndIdx].End = $"{tm / 60:D2}:{tm % 60:D2}"; SaveConfig(); ApplyConfig(); } RefreshWindowsSection(player, setEndIdx); return; } if (cmd == "setblockpct" && arg.Args.Length >= 3 && int.TryParse(arg.Args[1], out int setPctIdx) && setPctIdx >= 0 && setPctIdx < _config.ProtectedTimeWindows.Count && int.TryParse(arg.Args[2]?.Trim(), out int pct)) { _config.ProtectedTimeWindows[setPctIdx].BlockPercent = Mathf.Clamp(pct, 0, 100); SaveConfig(); ApplyConfig(); RefreshWindowsSection(player, setPctIdx); return; } if (cmd == "delwindow" && arg.Args.Length >= 2 && int.TryParse(arg.Args[1], out int windowIdx) && windowIdx >= 0 && windowIdx < _config.ProtectedTimeWindows.Count) { _config.ProtectedTimeWindows.RemoveAt(windowIdx); SaveConfig(); ApplyConfig(); int scrollTo = _config.ProtectedTimeWindows.Count == 0 ? 0 : Mathf.Min(windowIdx, _config.ProtectedTimeWindows.Count - 1); RefreshWindowsSection(player, scrollTo); return; } if (cmd == "addwindow") { _config.ProtectedTimeWindows.Add(new Configuration.TimeWindow { Start = "22:00", End = "06:00", Day = "Everyday", BlockPercent = 100 }); SaveConfig(); ApplyConfig(); RefreshWindowsSection(player, _config.ProtectedTimeWindows.Count - 1); return; } } /// Namn på språk-nøklar for /raid-meldingar. private static class Loc { public const string NoPermission = "NoPermission"; public const string RaidHeader = "RaidHeader"; public const string RaidCurrentTime = "RaidCurrentTime"; public const string RaidProtectedHoursLine = "RaidProtectedHoursLine"; public const string RaidBlockEndsIn = "RaidBlockEndsIn"; public const string RaidBlockStartsIn = "RaidBlockStartsIn"; } /// Registrerer standard engelske meldingar for /raid (kan oversettast med lang-fil). protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { [Loc.NoPermission] = "You don't have permission to use this command.", [Loc.RaidHeader] = "--- Raid block times ---", [Loc.RaidCurrentTime] = "Current time: {time} ({tz}) - {day}", [Loc.RaidProtectedHoursLine] = "Protected hours {start} - {end} - {pct}% ({day})", [Loc.RaidBlockEndsIn] = "Raid block ACTIVE - ends in {duration}", [Loc.RaidBlockStartsIn] = "Raid block OFF - starts in {duration}" }, this); } } }