/* * < ----- End-User License Agreement -----> * * You may not copy, modify, merge, publish, distribute, sublicense, or sell copies of this software without the developer’s consent. * * THIS SOFTWARE IS PROVIDED BY IIIaKa AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Developer: IIIaKa * https://t.me/iiiaka * Discord: @iiiaka * https://github.com/IIIaKa * https://umod.org/user/IIIaKa * https://codefling.com/iiiaka * https://www.patreon.com/iiiaka * https://boosty.to/iiiaka * Codefling plugin page: https://codefling.com/plugins/monuments-watcher * Codefling license: https://codefling.com/plugins/monuments-watcher?tab=downloads_field_4 * * Copyright © 2024 IIIaKa */ using System; using UnityEngine; using System.Collections.Generic; using System.Linq; using Facepunch; using Newtonsoft.Json; using Oxide.Core; using System.Globalization; using Oxide.Core.Libraries.Covalence; using Rust; namespace Oxide.Plugins { [Info("Monuments Watcher", "IIIaKa", "0.1.6")] [Description("A plugin creating a trigger box around Monuments and CargoShips to track entry and exit of players, npcs and entities from it.")] class MonumentsWatcher : RustPlugin { #region ~Variables~ private static MonumentsWatcher Instance { get; set; } private const string PERMISSION_ADMIN = "monumentswatcher.admin", Str_Leave = "leave", Str_Death = "death", Str_ClearList = "clearlist", Str_CargoShip = "CargoShip", HooksOnCargoWatcherCreated = "OnCargoWatcherCreated", HooksOnCargoWatcherDeleted = "OnCargoWatcherDeleted", HooksOnPlayerEnteredMonument = "OnPlayerEnteredMonument", HooksOnNpcEnteredMonument = "OnNpcEnteredMonument", HooksOnEntityEnteredMonument = "OnEntityEnteredMonument", HooksOnPlayerExitedMonument = "OnPlayerExitedMonument", HooksOnNpcExitedMonument = "OnNpcExitedMonument", HooksOnEntityExitedMonument = "OnEntityExitedMonument"; private Hash _monumentsList = new Hash(); private Hash> _playersInMonuments = new Hash>(); private Hash> _npcsInMonuments = new Hash>(); private Hash> _entitiesInMonuments = new Hash>(); private BoundsValues _cargoBounds; private readonly Dictionary _defaultBounds = new Dictionary() { { "CargoShip", new BoundsValues(new Vector3(0, 17, 10), new Vector3(26, 60, 147)) }, { "airfield_1", new BoundsValues(new Vector3(0, 15, -25), new Vector3(360, 60, 210)) }, { "arctic_research_base_a", new BoundsValues(new Vector3(-2, 15, -2), new Vector3(120, 40, 115)) }, { "bandit_town", new BoundsValues(new Vector3(0, 15, -5), new Vector3(160, 45, 160)) }, { "compound", new BoundsValues(new Vector3(0, 15, 0), new Vector3(180, 60, 200)) }, { "desert_military_base_a", new BoundsValues(new Vector3(0, 20, 3), new Vector3(100, 45, 100)) }, { "desert_military_base_b", new BoundsValues(new Vector3(0, 20, 3), new Vector3(100, 45, 100)) }, { "desert_military_base_c", new BoundsValues(new Vector3(0, 20, 3), new Vector3(100, 45, 100)) }, { "desert_military_base_d", new BoundsValues(new Vector3(0, 20, 3), new Vector3(100, 45, 100)) }, { "excavator_1", new BoundsValues(new Vector3(20, 35, -23), new Vector3(245, 100, 245)) }, { "ferry_terminal_1", new BoundsValues(new Vector3(4, 12, 18), new Vector3(215, 45, 205)) }, { "fishing_village_a", new BoundsValues(new Vector3(-2, 5, 0), new Vector3(85, 40, 90)) }, { "fishing_village_b", new BoundsValues(new Vector3(-3, 5, 4), new Vector3(60, 40, 90)) }, { "fishing_village_c", new BoundsValues(new Vector3(-3, 5, 4), new Vector3(60, 40, 90)) }, { "gas_station_1", new BoundsValues(new Vector3(0, 10, 14), new Vector3(70, 30, 60)) }, { "harbor_1", new BoundsValues(new Vector3(0, 20, 42), new Vector3(235, 60, 265)) }, { "harbor_1_old", new BoundsValues(new Vector3(0, 20, 15), new Vector3(235, 60, 210)) }, { "harbor_2", new BoundsValues(new Vector3(20, 20, 12), new Vector3(230, 60, 260)) }, { "harbor_2_old", new BoundsValues(new Vector3(10, 20, 15), new Vector3(220, 60, 250)) }, { "junkyard_1", new BoundsValues(new Vector3(0, 15, 10), new Vector3(180, 40, 180)) }, { "launch_site_1", new BoundsValues(new Vector3(10, 28, -25), new Vector3(555, 125, 290)) }, { "lighthouse", new BoundsValues(new Vector3(8, 35, 2), new Vector3(60, 80, 60)) }, { "military_tunnel_1", new BoundsValues(new Vector3(0, 20, -20), new Vector3(270, 85, 245)) }, { "mining_quarry_a", new BoundsValues(new Vector3(0, 10, 0), new Vector3(65, 25, 75)) }, { "mining_quarry_b", new BoundsValues(new Vector3(-5, 10, -2), new Vector3(65, 25, 60)) }, { "mining_quarry_c", new BoundsValues(new Vector3(-5, 10, 5), new Vector3(50, 25, 65)) }, { "nuclear_missile_silo", new BoundsValues(new Vector3(10, 15, 0), new Vector3(140, 100, 120)) }, { "oilrig_1", new BoundsValues(new Vector3(3, 30, 12), new Vector3(85, 110, 120)) }, { "oilrig_2", new BoundsValues(new Vector3(18, 15, -2), new Vector3(75, 80, 85)) }, { "powerplant_1", new BoundsValues(new Vector3(-15, 30, -13), new Vector3(220, 75, 300)) }, { "radtown_1", new BoundsValues(new Vector3(2.75f, 7.5f, 0.5f), new Vector3(125, 20, 80)) }, { "radtown_small_3", new BoundsValues(new Vector3(0, 20, -13), new Vector3(150, 55, 150)) }, { "satellite_dish", new BoundsValues(new Vector3(0, 25, 5), new Vector3(170, 60, 140)) }, { "sphere_tank", new BoundsValues(new Vector3(0, 41, 5), new Vector3(110, 85, 110)) }, { "stables_a", new BoundsValues(new Vector3(5, 15, -3), new Vector3(70, 30, 80)) }, { "stables_b", new BoundsValues(new Vector3(8, 15, 8), new Vector3(85, 30, 80)) }, { "station-sn-0", new BoundsValues(new Vector3(0, 8, 0), new Vector3(105, 18, 215)) }, { "station-sn-1", new BoundsValues(new Vector3(0, 8, 0), new Vector3(105, 18, 215)) }, { "station-sn-2", new BoundsValues(new Vector3(0, 8, 0), new Vector3(105, 18, 215)) }, { "station-sn-3", new BoundsValues(new Vector3(0, 8, 0), new Vector3(105, 18, 215)) }, { "station-we-0", new BoundsValues(new Vector3(0, 8, 0), new Vector3(215, 18, 105)) }, { "station-we-1", new BoundsValues(new Vector3(0, 8, 0), new Vector3(215, 18, 105)) }, { "station-we-2", new BoundsValues(new Vector3(0, 8, 0), new Vector3(215, 18, 105)) }, { "station-we-3", new BoundsValues(new Vector3(0, 8, 0), new Vector3(215, 18, 105)) }, { "supermarket_1", new BoundsValues(new Vector3(2, 5, 0), new Vector3(50, 15, 50)) }, { "swamp_a", new BoundsValues(new Vector3(-11, 15, 3), new Vector3(160, 35, 190)) }, { "swamp_b", new BoundsValues(new Vector3(-1, 15, -3), new Vector3(125, 35, 125)) }, { "swamp_c", new BoundsValues(new Vector3(6, 15, -1), new Vector3(130, 35, 130)) }, { "trainyard_1", new BoundsValues(new Vector3(5, 25, -5), new Vector3(250, 80, 230)) }, { "underwater_lab_a", new BoundsValues(new Vector3(0, 15, 0), new Vector3(110, 25, 110)) }, { "underwater_lab_b", new BoundsValues(new Vector3(0, 15, 0), new Vector3(110, 25, 110)) }, { "underwater_lab_c", new BoundsValues(new Vector3(0, 15, 0), new Vector3(110, 25, 110)) }, { "underwater_lab_d", new BoundsValues(new Vector3(0, 15, 0), new Vector3(110, 25, 110)) }, { "warehouse", new BoundsValues(new Vector3(0, 5, -7), new Vector3(45, 15, 30)) }, { "water_treatment_plant_1", new BoundsValues(new Vector3(0, 30, -30), new Vector3(250, 90, 290)) }, { "entrance_bunker_a", new BoundsValues(new Vector3(-4, 1, -1), new Vector3(20, 30, 20)) }, { "entrance_bunker_b", new BoundsValues(new Vector3(-8, 1, 0), new Vector3(30, 30, 20)) }, { "entrance_bunker_c", new BoundsValues(new Vector3(-4, 1, -1), new Vector3(20, 30, 20)) }, { "entrance_bunker_d", new BoundsValues(new Vector3(-4, 1, -1), new Vector3(20, 30, 20)) }, { "cave_small_easy", new BoundsValues(new Vector3(6, -28, 17), new Vector3(45, 46, 66)) }, { "cave_small_medium", new BoundsValues(new Vector3(20, -30, -18), new Vector3(80, 50, 65)) }, { "cave_small_hard", new BoundsValues(new Vector3(8, -21, 0), new Vector3(45, 35, 80)) }, { "cave_medium_easy", new BoundsValues(new Vector3(8, -21, 0), new Vector3(45, 35, 80)) }, { "cave_medium_medium", new BoundsValues(new Vector3(-1, -25, 2), new Vector3(110, 50, 110)) }, { "cave_medium_hard", new BoundsValues(new Vector3(8, -21, 0), new Vector3(45, 35, 80)) }, { "cave_large_medium", new BoundsValues(new Vector3(8, -21, 0), new Vector3(45, 35, 80)) }, { "cave_large_hard", new BoundsValues(new Vector3(8, -21, 0), new Vector3(45, 35, 80)) }, { "cave_large_sewers_hard", new BoundsValues(new Vector3(50, -25, -7), new Vector3(170, 40, 165)) }, { "ice_lake_1", new BoundsValues(new Vector3(-2, 15, 0), new Vector3(140, 35, 160)) }, { "ice_lake_2", new BoundsValues(new Vector3(0, 15, 0), new Vector3(150, 35, 150)) }, { "ice_lake_3", new BoundsValues(new Vector3(0, 15, 0), new Vector3(180, 35, 240)) }, { "ice_lake_4", new BoundsValues(new Vector3(0, 15, 0), new Vector3(85, 35, 85)) }, { "power_sub_small_1", new BoundsValues(new Vector3(0, 5, 0), new Vector3(15, 10, 15)) }, { "power_sub_small_2", new BoundsValues(new Vector3(0, 5, 0), new Vector3(15, 10, 15)) }, { "power_sub_big_1", new BoundsValues(new Vector3(0, 5, 1), new Vector3(20, 10, 22)) }, { "power_sub_big_2", new BoundsValues(new Vector3(-1, 5, 1), new Vector3(23, 10, 22)) }, { "water_well_a", new BoundsValues(new Vector3(-2, 7, 0), new Vector3(25, 20, 25)) }, { "water_well_b", new BoundsValues(new Vector3(-1, 7, 0), new Vector3(25, 20, 25)) }, { "water_well_c", new BoundsValues(new Vector3(0, 10, 1), new Vector3(30, 25, 30)) }, { "water_well_d", new BoundsValues(new Vector3(0, 10, 1), new Vector3(30, 25, 30)) }, { "water_well_e", new BoundsValues(new Vector3(-1, 7, 0), new Vector3(25, 20, 25)) }, { "monument_marker", new BoundsValues(new Vector3(0, 0, 0), new Vector3(100, 100, 100)) } }; #endregion #region ~Configuration~ private static Configuration _config; private class Configuration { [JsonProperty(PropertyName = "Chat command")] public string Command = "monument"; [JsonProperty(PropertyName = "Use GameTip for messages?")] public bool Use_GameTips = true; [JsonProperty(PropertyName = "Is it worth recreating boundaries(excluding custom monuments) upon detecting a wipe?")] public bool Wipe_Recreate = true; [JsonProperty(PropertyName = "List of tracked types of monuments. Leave blank to track all")] public HashSet Tracked_Types = new HashSet(); [JsonProperty(PropertyName = "Wipe ID")] public string WipeID = ""; public Oxide.Core.VersionNumber Version; } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); } catch (Exception ex) { PrintError($"{ex.Message}\n\n[{Title}] Your configuration file contains an error."); } if (_config == null || _config.Version == new VersionNumber()) { PrintWarning("The configuration file is not found or contains errors. Creating a new one..."); LoadDefaultConfig(); } else if (_config.Version < Version) { string wipeID = _config.WipeID; PrintWarning($"Your configuration file version({_config.Version}) is outdated. Updating it to {Version}."); LoadDefaultConfig(); if (!string.IsNullOrWhiteSpace(wipeID)) _config.WipeID = wipeID; PrintWarning($"The configuration file has been successfully updated to version {_config.Version}!"); } SaveConfig(); } protected override void SaveConfig() => Config.WriteObject(_config); protected override void LoadDefaultConfig() => _config = new Configuration() { Version = Version }; #endregion #region ~Language~ private bool _langReady = false; private Dictionary _enLang = new Dictionary { ["MsgMonumentCreated"] = "Key {0} created at coordinates {1}. You need to specify the required data in the configuration file and reload the plugin.", ["MsgMonumentRecreated"] = "The boundaries of the monuments have been successfully recreated!", ["MsgMonumentCreateNoPosition"] = "You did not specify the position or specified it incorrectly!", ["MsgMonumentKeyNotFound"] = "Key {0} not found!", ["MsgMonumentRotated"] = "Successful rotation of the {0} by Y-coordinate({1})!", ["MsgMonumentRotationNotFound"] = "You should be at the monument and facing the right direction. Otherwise, provide the monument's ID and Y-coordinate.", ["MsgMonumentShowNotFound"] = "You should be at the monument. Otherwise, provide the ID or name of the monument.", ["MsgMonumentShow"] = "{0} is located at coordinates: {1}", ["MsgMonumentShowList"] = "Found and displayed {0} monuments with key {1}", ["MsgMonumentShowNoPlayer"] = "Monument display works only for players!", ["CargoShip"] = "CargoShip", ["airfield_1"] = "Airfield", ["airfield_1_station"] = "Airfield Station", ["arctic_research_base_a"] = "Arctic Research Base", ["arctic_research_base_a_station"] = "Arctic Station", ["bandit_town"] = "Bandit Camp", ["bandit_town_station"] = "Bandit Station", ["compound"] = "Outpost", ["compound_station"] = "Outpost Station", ["desert_military_base_a"] = "Abandoned Military Base", ["desert_military_base_a_station"] = "Dune Station", ["desert_military_base_b"] = "Abandoned Military Base", ["desert_military_base_b_station"] = "Dune Station", ["desert_military_base_c"] = "Abandoned Military Base", ["desert_military_base_c_station"] = "Dune Station", ["desert_military_base_d"] = "Abandoned Military Base", ["desert_military_base_d_station"] = "Dune Station", ["excavator_1"] = "Giant Excavator Pit", ["excavator_1_station"] = "Excavator Station", ["ferry_terminal_1"] = "Ferry Terminal", ["ferry_terminal_1_station"] = "Ferry Terminal Station", ["fishing_village_a"] = "Large Fishing Village", ["fishing_village_a_station"] = "Large Fishing Station", ["fishing_village_b"] = "Fishing Village", ["fishing_village_b_station"] = "Fishing Station", ["fishing_village_c"] = "Fishing Village", ["fishing_village_c_station"] = "Fishing Station", ["gas_station_1"] = "Oxum's Gas Station", ["harbor_1"] = "Large Harbor", ["harbor_1_station"] = "Large Harbor Station", ["harbor_2"] = "Small Harbor", ["harbor_2_station"] = "Harbor Station", ["junkyard_1"] = "Junkyard", ["junkyard_1_station"] = "Junkyard Station", ["launch_site_1"] = "Launch Site", ["launch_site_1_station"] = "Launch Site Station", ["lighthouse"] = "Lighthouse", ["military_tunnel_1"] = "Military Tunnel", ["military_tunnel_1_station"] = "Military Tunnel Station", ["mining_quarry_a"] = "Sulfur Quarry", ["mining_quarry_b"] = "Stone Quarry", ["mining_quarry_c"] = "HQM Quarry", ["nuclear_missile_silo"] = "Missile Silo", ["nuclear_missile_silo_station"] = "Silo Station", ["oilrig_1"] = "Large Oil Rig", ["oilrig_2"] = "Oil Rig", ["powerplant_1"] = "Power Plant", ["powerplant_1_station"] = "Power Plant Station", ["radtown_1"] = "Toxic Village", ["radtown_1_station"] = "Toxic Village Station", ["radtown_small_3"] = "Sewer Branch", ["radtown_small_3_station"] = "Sewer Branch Station", ["satellite_dish"] = "Satellite Dish", ["satellite_dish_station"] = "Satellite Station", ["sphere_tank"] = "The Dome", ["sphere_tank_station"] = "The Dome Station", ["stables_a"] = "Ranch", ["stables_a_station"] = "Ranch Station", ["stables_b"] = "Large Barn", ["stables_b_station"] = "Barn Station", ["station-sn-0"] = "Tunnel Station", ["station-sn-1"] = "Tunnel Station", ["station-sn-2"] = "Tunnel Station", ["station-sn-3"] = "Tunnel Station", ["station-we-0"] = "Tunnel Station", ["station-we-1"] = "Tunnel Station", ["station-we-2"] = "Tunnel Station", ["station-we-3"] = "Tunnel Station", ["supermarket_1"] = "Abandoned Supermarket", ["swamp_a"] = "Wild Swamp", ["swamp_b"] = "Wild Swamp", ["swamp_c"] = "Abandoned Cabins", ["trainyard_1"] = "Train Yard", ["trainyard_1_station"] = "Train Yard Station", ["underwater_lab_a"] = "Underwater Lab", ["underwater_lab_b"] = "Underwater Lab", ["underwater_lab_c"] = "Underwater Lab", ["underwater_lab_d"] = "Underwater Lab", ["warehouse"] = "Mining Outpost", ["water_treatment_plant_1"] = "Water Treatment Plant", ["water_treatment_plant_1_station"] = "Water Treatment Station", ["entrance_bunker_a"] = "Bunker Entrance", ["entrance_bunker_b"] = "Bunker Entrance", ["entrance_bunker_c"] = "Bunker Entrance", ["entrance_bunker_d"] = "Bunker Entrance", ["cave_small_easy"] = "Small Cave", ["cave_small_medium"] = "Medium Cave", ["cave_small_hard"] = "Medium Cave", ["cave_medium_easy"] = "Medium Cave", ["cave_medium_medium"] = "Medium Cave", ["cave_medium_hard"] = "Medium Cave", ["cave_large_medium"] = "Medium Cave", ["cave_large_hard"] = "Medium Cave", ["cave_large_sewers_hard"] = "Large Cave", ["ice_lake_1"] = "Ice Lake", ["ice_lake_2"] = "Ice Lake", ["ice_lake_3"] = "Large Ice Lake", ["ice_lake_4"] = "Small Ice Lake", ["power_sub_small_1"] = "Substation", ["power_sub_small_2"] = "Substation", ["power_sub_big_1"] = "Large Substation", ["power_sub_big_2"] = "Large Substation", ["water_well_a"] = "Water Well", ["water_well_b"] = "Water Well", ["water_well_c"] = "Water Well", ["water_well_d"] = "Water Well", ["water_well_e"] = "Water Well" }; private Dictionary _ruLang = new Dictionary { ["MsgMonumentCreated"] = "Ключ {0} создан по координатам {1}. Вам необходимо в конфиг файле указать необходимые данные и перезагрузить плагин.", ["MsgMonumentRecreated"] = "Границы монументов успешно пересозданы!", ["MsgMonumentCreateNoPosition"] = "Вы не указали позицию, либо указали ее не правильно!", ["MsgMonumentKeyNotFound"] = "Ключ {0} не найден!", ["MsgMonumentRotated"] = "Успешный поворот у {0} по Y координате({1})!", ["MsgMonumentRotationNotFound"] = "Вы должны находится в монументе и смотреть в нужное направление. Либо указать ID монумента и Y координату.", ["MsgMonumentShowNotFound"] = "Вы должны находится в монументе. Либо указать ID или имя монумента.", ["MsgMonumentShow"] = "{0} расположен по координатам: {1}", ["MsgMonumentShowList"] = "Найдено и отображено {0} монументов с ключом {1}", ["MsgMonumentShowNoPlayer"] = "Отображение монументов работает только для игроков!", ["CargoShip"] = "Грузовой корабль", ["airfield_1"] = "Аэропорт", ["airfield_1_station"] = "Станция Аэропорт", ["arctic_research_base_a"] = "Арктическая база", ["arctic_research_base_a_station"] = "Станция Арктическая", ["bandit_town"] = "Лагерь бандитов", ["bandit_town_station"] = "Станция бандитов", ["compound"] = "Город", ["compound_station"] = "Станция Город", ["desert_military_base_a"] = "Заброшенная военная база", ["desert_military_base_a_station"] = "Станция Дюна", ["desert_military_base_b"] = "Заброшенная военная база", ["desert_military_base_b_station"] = "Станция Дюна", ["desert_military_base_c"] = "Заброшенная военная база", ["desert_military_base_c_station"] = "Станция Дюна", ["desert_military_base_d"] = "Заброшенная военная база", ["desert_military_base_d_station"] = "Станция Дюна", ["excavator_1"] = "Гигантский экскаватор", ["excavator_1_station"] = "Станция Экскаваторная", ["ferry_terminal_1"] = "Паромный терминал", ["ferry_terminal_1_station"] = "Станция Паромщиков", ["fishing_village_a"] = "Большая рыбацкая деревня", ["fishing_village_a_station"] = "Станция Рыбаков", ["fishing_village_b"] = "Рыбацкая деревня", ["fishing_village_b_station"] = "Станция Рыбаков", ["fishing_village_c"] = "Рыбацкая деревня", ["fishing_village_c_station"] = "Станция Рыбаков", ["gas_station_1"] = "Заправка", ["harbor_1"] = "Большой порт", ["harbor_1_station"] = "Станция Моряков", ["harbor_2"] = "Порт", ["harbor_2_station"] = "Станция Моряков", ["junkyard_1"] = "Свалка", ["junkyard_1_station"] = "Станция Мусорщиков", ["launch_site_1"] = "Космодром", ["launch_site_1_station"] = "Станция Космонавтов", ["lighthouse"] = "Маяк", ["military_tunnel_1"] = "Военные туннели", ["military_tunnel_1_station"] = "Станция Туннельная", ["mining_quarry_a"] = "Серный карьер", ["mining_quarry_b"] = "Каменный карьер", ["mining_quarry_c"] = "МВК карьер", ["nuclear_missile_silo"] = "Ракетная пусковая шахта", ["nuclear_missile_silo_station"] = "Станция Ракетная", ["oilrig_1"] = "Большая нефтяная вышка", ["oilrig_2"] = "Нефтяная вышка", ["powerplant_1"] = "Электростанция", ["powerplant_1_station"] = "Станция Электриков", ["radtown_1"] = "Токсичная деревня", ["radtown_1_station"] = "Станция Легаси", ["radtown_small_3"] = "Канализационный отвод", ["radtown_small_3_station"] = "Станция Отвод", ["satellite_dish"] = "Спутниковая тарелка", ["satellite_dish_station"] = "Станция Связистов", ["sphere_tank"] = "Сфера", ["sphere_tank_station"] = "Станция Сфера", ["stables_a"] = "Ранчо", ["stables_a_station"] = "Станция Ранчо", ["stables_b"] = "Большой амбар", ["stables_b_station"] = "Станция Амбарная", ["station-sn-0"] = "Станция метро", ["station-sn-1"] = "Станция метро", ["station-sn-2"] = "Станция метро", ["station-sn-3"] = "Станция метро", ["station-we-0"] = "Станция метро", ["station-we-1"] = "Станция метро", ["station-we-2"] = "Станция метро", ["station-we-3"] = "Станция метро", ["supermarket_1"] = "Супермаркет", ["swamp_a"] = "Болото", ["swamp_b"] = "Болото", ["swamp_c"] = "Заброшенные хижины", ["trainyard_1"] = "Железнодорожное депо", ["trainyard_1_station"] = "Станция Железнодорожников", ["underwater_lab_a"] = "Подводная лаборатория", ["underwater_lab_b"] = "Подводная лаборатория", ["underwater_lab_c"] = "Подводная лаборатория", ["underwater_lab_d"] = "Подводная лаборатория", ["warehouse"] = "Склад", ["water_treatment_plant_1"] = "Очистные сооружения", ["water_treatment_plant_1_station"] = "Станция Очистная", ["entrance_bunker_a"] = "Вход в бункер", ["entrance_bunker_b"] = "Вход в бункер", ["entrance_bunker_c"] = "Вход в бункер", ["entrance_bunker_d"] = "Вход в бункер", ["cave_small_easy"] = "Маленькая пещера", ["cave_small_medium"] = "Средняя пещера", ["cave_small_hard"] = "Средняя пещера", ["cave_medium_easy"] = "Средняя пещера", ["cave_medium_medium"] = "Средняя пещера", ["cave_medium_hard"] = "Средняя пещера", ["cave_large_medium"] = "Средняя пещера", ["cave_large_hard"] = "Средняя пещера", ["cave_large_sewers_hard"] = "Большая пещера", ["ice_lake_1"] = "Замерзшее озеро", ["ice_lake_2"] = "Замерзшее озеро", ["ice_lake_3"] = "Большое замерзшее озеро", ["ice_lake_4"] = "Маленькое замерзшее озеро", ["power_sub_small_1"] = "Подстанция", ["power_sub_small_2"] = "Подстанция", ["power_sub_big_1"] = "Большая подстанция", ["power_sub_big_2"] = "Большая подстанция", ["water_well_a"] = "Колодец с водой", ["water_well_b"] = "Колодец с водой", ["water_well_c"] = "Колодец с водой", ["water_well_d"] = "Колодец с водой", ["water_well_e"] = "Колодец с водой" }; protected override void LoadDefaultMessages() { if (!_langReady) { timer.Once(1f, LoadDefaultMessages); return; } lang.RegisterMessages(_enLang, this); lang.RegisterMessages(_ruLang, this, "ru"); _enLang.Clear(); _ruLang.Clear(); } #endregion #region ~Methods~ private void InitMonuments() { LoadBoundsConfig(); LoadCustomBoundsConfig(); _playersInMonuments.Clear(); _entitiesInMonuments.Clear(); string monumentKey, prefab; int miningoutpost = 0, lighthouse = 0, gasstation = 0, supermarket = 0, tunnel = 0, bunker = 0, cave = 0, icelake = 0, power = 0, waterwell = 0; if (!_monumentsBounds.ContainsKey(Str_CargoShip)) { _monumentsBounds[Str_CargoShip] = new MonumentBounds() { Center = Vector3.zero, CenterOffset = _defaultBounds[Str_CargoShip].CenterOffset, Size = _defaultBounds[Str_CargoShip].Size, Rotation = Vector3.zero }; } _cargoBounds = new BoundsValues(_monumentsBounds[Str_CargoShip].CenterOffset, _monumentsBounds[Str_CargoShip].Size); foreach (var entity in BaseNetworkable.serverEntities) { if (entity is CargoShip cargoShip) CreateCargoWatcher(cargoShip); } foreach (var monument in TerrainMeta.Path.Monuments) { prefab = monument.name.ToLower(); if (prefab.Contains("monument_marker.prefab")) { monumentKey = monument.transform.root.name.ToLower(); monumentKey = System.Text.RegularExpressions.Regex.Replace(monumentKey, @"[^\w\d]", ""); CreateCustomWatcher(monumentKey, monument.transform, prefab, monument.transform.root.name); continue; } monumentKey = ClearMonumentName(prefab); if (!_defaultBounds.ContainsKey(monumentKey)) continue; if (monument.IsSafeZone) { CreateWatcher(monumentKey, WatcherMonumentType.SafeZone, monument.transform, prefab); continue; } switch (monumentKey) { case "oilrig_1": case "oilrig_2": case "underwater_lab_a": case "underwater_lab_b": case "underwater_lab_c": case "underwater_lab_d": CreateWatcher(monumentKey, WatcherMonumentType.RadTownWater, monument.transform, prefab); break; case "lighthouse": lighthouse++; CreateWatcher(monumentKey, WatcherMonumentType.RadTownSmall, monument.transform, prefab, $"_{lighthouse}", $"#{lighthouse}"); break; case "gas_station_1": gasstation++; CreateWatcher(monumentKey, WatcherMonumentType.RadTownSmall, monument.transform, prefab, $"_{gasstation}", $"#{gasstation}"); break; case "supermarket_1": supermarket++; CreateWatcher(monumentKey, WatcherMonumentType.RadTownSmall, monument.transform, prefab, $"_{supermarket}", $"#{supermarket}"); break; case "warehouse": miningoutpost++; CreateWatcher(monumentKey, WatcherMonumentType.RadTownSmall, monument.transform, prefab, $"_{miningoutpost}", $"#{miningoutpost}"); break; case "mining_quarry_a": case "mining_quarry_b": case "mining_quarry_c": CreateWatcher(monumentKey, WatcherMonumentType.MiningQuarry, monument.transform, prefab); break; case "swamp_a": case "swamp_b": case "swamp_c": CreateWatcher(monumentKey, WatcherMonumentType.Swamp, monument.transform, prefab); break; case "entrance_bunker_a": case "entrance_bunker_b": case "entrance_bunker_c": case "entrance_bunker_d": bunker++; CreateWatcher(monumentKey, WatcherMonumentType.BunkerEntrance, monument.transform, prefab, $"_{bunker}", $"#{bunker}"); break; case "cave_small_easy": case "cave_small_medium": case "cave_small_hard": case "cave_medium_easy": case "cave_medium_medium": case "cave_medium_hard": case "cave_large_medium": case "cave_large_hard": case "cave_large_sewers_hard": cave++; CreateWatcher(monumentKey, WatcherMonumentType.Cave, monument.transform, prefab, $"_{cave}", $"#{cave}"); break; case "ice_lake_1": case "ice_lake_2": case "ice_lake_3": case "ice_lake_4": icelake++; CreateWatcher(monumentKey, WatcherMonumentType.IceLake, monument.transform, prefab, $"_{icelake}", $"#{icelake}"); break; case "power_sub_small_1": case "power_sub_small_2": case "power_sub_big_1": case "power_sub_big_2": power++; CreateWatcher(monumentKey, WatcherMonumentType.PowerSubstation, monument.transform, prefab, $"_{power}", $"#{power}"); break; case "water_well_a": case "water_well_b": case "water_well_c": case "water_well_d": case "water_well_e": waterwell++; CreateWatcher(monumentKey, WatcherMonumentType.WaterWell, monument.transform, prefab, $"_{waterwell}", $"#{waterwell}"); break; default: CreateWatcher(monumentKey, WatcherMonumentType.RadTown, monument.transform, prefab); break; } } float stationDistance = 100f; foreach (var station in TerrainMeta.Path.DungeonGridCells) { if (!station.name.Contains("/tunnel-station/station-", CompareOptions.IgnoreCase)) continue; prefab = station.name.ToLower(); monumentKey = ClearMonumentName(prefab); MonumentWatcher parentWatcher = null; var groundPos = new Vector3(station.transform.position.x, TerrainMeta.HeightMap.GetHeight(station.transform.position), station.transform.position.z); foreach (var monument in _monumentsList.Values) { if (monument.Type != WatcherMonumentType.SafeZone && monument.Type != WatcherMonumentType.RadTown) continue; if (Vector3.Distance(monument.boxCollider.ClosestPointOnBounds(groundPos), groundPos) <= stationDistance) parentWatcher = monument; } if (parentWatcher != null) { CreateWatcher(monumentKey, WatcherMonumentType.TunnelStation, station.transform, prefab, $"_{parentWatcher.ID}"); _monumentsList[$"{monumentKey}_{parentWatcher.ID}"].LangKey = $"{parentWatcher.ID}_station"; } else { tunnel++; CreateWatcher(monumentKey, WatcherMonumentType.TunnelStation, station.transform, prefab, $"_{tunnel}", $"#{tunnel}"); } } Subscribe(nameof(OnEntitySpawned)); Subscribe(nameof(OnEntityDeath)); Subscribe(nameof(OnEntityKill)); Subscribe(nameof(OnPlayerTeleported)); SaveBoundsConfig(); SaveCustomBoundsConfig(); FreeBoundsConfig(); _langReady = true; } private void CreateCargoWatcher(CargoShip cargoShip) { if (cargoShip == null) return; ulong cargoID = cargoShip.net.ID.Value; var type = WatcherMonumentType.RadTownWater; if (_config.Tracked_Types.Any() && !_config.Tracked_Types.Contains(type.ToString())) return; string monumentID = $"CargoShip_{cargoID}"; var watcher = new GameObject().gameObject.AddComponent(); watcher.InitializeProperties(monumentID, type, cargoShip.name, Str_CargoShip, $"#{cargoID}", isMoveable: true); watcher.InitializeCargoShip(cargoShip); _monumentsList[monumentID] = watcher; watcher.transform.parent = cargoShip.transform; Interface.CallHook(HooksOnCargoWatcherCreated, monumentID, type.ToString()); } private void CreateWatcher(string monumentKey, WatcherMonumentType type, Transform transform, string prefab, string idSuffix = "", string suffix = "") { if (_config.Tracked_Types.Any() && !_config.Tracked_Types.Contains(type.ToString())) return; string monumentID = $"{monumentKey}{(!string.IsNullOrWhiteSpace(idSuffix) ? idSuffix : string.Empty)}"; if (!_monumentsBounds.TryGetValue(monumentID, out var bounds)) { _monumentsBounds[monumentID] = bounds = new MonumentBounds() { Center = transform.position, CenterOffset = _defaultBounds[monumentKey].CenterOffset, Size = _defaultBounds[monumentKey].Size, Rotation = transform.rotation.eulerAngles }; } var watcher = new GameObject().AddComponent(); watcher.InitializeProperties(monumentID, type, prefab, monumentKey, suffix); watcher.InitializeMonument(bounds); _monumentsList[monumentID] = watcher; } private void CreateCustomWatcher(string monumentID, Transform transform, string prefab, string displayName) { _enLang[$"custom_{monumentID}"] = displayName; _ruLang[$"custom_{monumentID}"] = displayName; if (!_customMonumentsBounds.TryGetValue(monumentID, out var bounds)) { var rotation = transform.rotation.eulerAngles; var colArray = new Collider[5]; var count = Physics.OverlapSphereNonAlloc(transform.position, 1, colArray, Rust.Layers.Mask.Prevent_Building, QueryTriggerInteraction.Ignore); for (var i = 0; i < count; i++) { var collider = colArray[i]; if (collider.name.Contains("prevent_building", CompareOptions.IgnoreCase)) { rotation = collider.transform.rotation.eulerAngles; break; } } _customMonumentsBounds[monumentID] = bounds = new CustomMonumentBounds() { MonumentType = WatcherMonumentType.Custom, Center = transform.position, CenterOffset = _defaultBounds["monument_marker"].CenterOffset, Size = _defaultBounds["monument_marker"].Size, Rotation = rotation }; } if (bounds.MonumentType != WatcherMonumentType.Custom && _config.Tracked_Types.Any() && !_config.Tracked_Types.Contains(bounds.MonumentType.ToString())) return; var watcher = new GameObject().AddComponent(); watcher.InitializeProperties(monumentID, bounds.MonumentType, prefab, $"custom_{monumentID}", isCustom: true); watcher.InitializeMonument(bounds); _monumentsList[monumentID] = watcher; } private void ClearWatchers() { Unsubscribe(nameof(OnEntitySpawned)); Unsubscribe(nameof(OnEntityDeath)); Unsubscribe(nameof(OnEntityKill)); Unsubscribe(nameof(OnPlayerTeleported)); foreach (var kvp in _monumentsList.ToList()) UnityEngine.Object.DestroyImmediate(kvp.Value.gameObject); _monumentsList.Clear(); _playersInMonuments.Clear(); _npcsInMonuments.Clear(); _entitiesInMonuments.Clear(); } private static string ClearMonumentName(string prefabName) { prefabName = prefabName.Replace(".prefab", ""); string[] parts = prefabName.Split('/'); return parts[^1]; } private WatcherMonumentType ConvertStringToEnum(string enumString) { if (Enum.TryParse(enumString, out WatcherMonumentType parsedEnum)) return parsedEnum; PrintWarning($"Unable to convert to WatcherMonumentType: {enumString}"); return WatcherMonumentType.RadTown; } private Vector3[] GetBoxCorners(BoxCollider boxCollider) { Vector3[] corners = new Vector3[8]; Vector3 center = boxCollider.center; Vector3 size = boxCollider.size * 0.5f; Vector3[] offsets = new Vector3[8] { new Vector3(-size.x, -size.y, -size.z), new Vector3(size.x, -size.y, -size.z), new Vector3(size.x, -size.y, size.z), new Vector3(-size.x, -size.y, size.z), new Vector3(-size.x, size.y, -size.z), new Vector3(size.x, size.y, -size.z), new Vector3(size.x, size.y, size.z), new Vector3(-size.x, size.y, size.z) }; for (int i = 0; i < offsets.Length; i++) corners[i] = boxCollider.transform.TransformPoint(center + offsets[i]); return corners; } private Dictionary> GetLinesBetweenCorners(Vector3[] corners) { Dictionary> linesDictionary = new Dictionary>(); for (int i = 0; i < corners.Length; i++) { Vector3 startPoint = corners[i]; List otherPoints = new List(corners); otherPoints.RemoveAt(i); linesDictionary.Add(startPoint, otherPoints); } return linesDictionary; } private void UpdateFlag(BasePlayer player, BasePlayer.PlayerFlags flag, bool addFlag) { if (player != null) { player.SetPlayerFlag(flag, addFlag); player.SendNetworkUpdateImmediate(); } } private void SendMessage(IPlayer player, string replyKey, string[] replyArgs = null, bool isWarning = false) { if (player == null) return; if (replyArgs == null) replyArgs = new string[0]; if (!player.IsServer && _config.Use_GameTips) player.Command("gametip.showtoast", (int)(isWarning ? GameTip.Styles.Error : GameTip.Styles.Blue_Normal), string.Format(lang.GetMessage(replyKey, this, player.Id), replyArgs), string.Empty); else player.Reply(string.Format(lang.GetMessage(replyKey, this, player.Id), replyArgs)); } #endregion #region ~API~ private string GetMonumentDisplayNameByLang(string monumentID, string langKey = "en", bool showSuffix = true) { if (string.IsNullOrWhiteSpace(langKey)) langKey = "en"; //https://github.com/OxideMod/Oxide.Core/pull/100 /*if (_monumentsList.TryGetValue(monumentID, out var watcher)) return $"{lang.GetMessageByLang(watcher.LangKey, this, langKey)}{(showSuffix && !string.IsNullOrWhiteSpace(watcher.Suffix) ? $" {watcher.Suffix}" : string.Empty)}"; return string.Empty;*/ return monumentID; } private string GetMonumentDisplayName(string monumentID, object obj, bool showSuffix = true) => GetMonumentDisplayName(monumentID, ulong.TryParse(obj.ToString(), out var userID) ? userID.ToString() : "", showSuffix); private string GetMonumentDisplayName(string monumentID, ulong userID, bool showSuffix = true) => GetMonumentDisplayName(monumentID, userID.ToString(), showSuffix); private string GetMonumentDisplayName(string monumentID, string userID = "", bool showSuffix = true) { if (userID == "0") userID = string.Empty; if (_monumentsList.TryGetValue(monumentID, out var watcher)) return $"{lang.GetMessage(watcher.LangKey, this, userID)}{(showSuffix && !string.IsNullOrWhiteSpace(watcher.Suffix) ? $" {watcher.Suffix}" : string.Empty)}"; return string.Empty; } private string GetMonumentType(string monumentID) => _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.TypeString : string.Empty; private Vector3 GetMonumentPosition(string monumentID) => _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.transform.position : Vector3.zero; private string[] GetMonumentsList() => _monumentsList.Keys.ToArray(); private Dictionary GetMonumentsTypeDictionary() => _monumentsList.ToDictionary(monument => monument.Key, monument => monument.Value.TypeString); private string[] GetMonumentsByType(string type) => _monumentsList.Where(monument => monument.Value.Type == ConvertStringToEnum(type)).Select(monument => monument.Key).ToArray(); private object GetMonumentPlayers(string monumentID) => _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.PlayersList : null; private object GetMonumentNpcs(string monumentID) => _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.NpcsList : null; private object GetMonumentEntities(string monumentID) => _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.EntitiesList : null; private string GetPlayerMonument(object obj) => ulong.TryParse(obj.ToString(), out var userID) ? GetPlayerMonument(userID) : string.Empty; private string GetPlayerMonument(BasePlayer player) => GetPlayerMonument(player.userID); private string GetPlayerMonument(ulong userID) { if (_playersInMonuments.TryGetValue(userID, out var watchers) && watchers.Any()) return watchers[^1].ID; return string.Empty; } private string GetNpcMonument(BasePlayer npcPlayer) => npcPlayer != null && npcPlayer.net != null ? GetNpcMonument(npcPlayer.net.ID) : string.Empty; private string GetNpcMonument(NetworkableId netID) { if (_npcsInMonuments.TryGetValue(netID, out var watchers) && watchers.Any()) return watchers[^1].ID; return string.Empty; } private string GetEntityMonument(BaseEntity entity) => entity != null && entity.net != null ? GetEntityMonument(entity.net.ID) : string.Empty; private string GetEntityMonument(NetworkableId netID) { if (_entitiesInMonuments.TryGetValue(netID, out var watchers) && watchers.Any()) return watchers[^1].ID; return string.Empty; } private object GetPlayerMonuments(object obj) => ulong.TryParse(obj.ToString(), out var userID) ? GetPlayerMonuments(userID) : null; private object GetPlayerMonuments(BasePlayer player) => GetPlayerMonuments(player.userID); private object GetPlayerMonuments(ulong userID) { if (_playersInMonuments.TryGetValue(userID, out var watchers) && watchers.Any()) { int total = watchers.Count; string[] result = new string[total]; for (int i = 0; i < total; i++) result[i] = watchers[i].ID; return result; } return null; } private object GetNpcMonuments(BasePlayer npcPlayer) => npcPlayer != null && npcPlayer.net != null ? GetNpcMonuments(npcPlayer.net.ID) : string.Empty; private object GetNpcMonuments(NetworkableId netID) { if (_npcsInMonuments.TryGetValue(netID, out var watchers) && watchers.Any()) { int total = watchers.Count; string[] result = new string[total]; for (int i = 0; i < total; i++) result[i] = watchers[i].ID; return result; } return null; } private object GetEntityMonuments(BaseEntity entity) => entity != null && entity.net != null ? GetEntityMonuments(entity.net.ID) : string.Empty; private object GetEntityMonuments(NetworkableId netID) { if (_entitiesInMonuments.TryGetValue(netID, out var watchers) && watchers.Any()) { int total = watchers.Count; string[] result = new string[total]; for (int i = 0; i < total; i++) result[i] = watchers[i].ID; return result; } return null; } private string GetMonumentByPos(Vector3 pos) { foreach (var watcher in _monumentsList.Values) { if (watcher.IsInBounds(pos)) return watcher.ID; } return string.Empty; } private object GetMonumentsByPos(Vector3 pos) { var result = new List(); foreach (var watcher in _monumentsList.Values) { if (watcher.IsInBounds(pos)) result.Add(watcher.ID); } if (result.Any()) return result.ToArray(); return null; } private bool IsPosInMonument(string monumentID, Vector3 pos) { if (_monumentsList.TryGetValue(monumentID, out var watcher)) return watcher.IsInBounds(pos); return false; } private bool IsPlayerInMonument(string monumentID, object obj) => ulong.TryParse(obj.ToString(), out var userID) ? IsPlayerInMonument(monumentID, userID) : false; private bool IsPlayerInMonument(string monumentID, ulong userID) => IsPlayerInMonument(monumentID, BasePlayer.FindAwakeOrSleepingByID(userID)); private bool IsPlayerInMonument(string monumentID, BasePlayer player) { if (player != null && _monumentsList.TryGetValue(monumentID, out var watcher)) return watcher.PlayersList.Contains(player); return false; } private bool IsNpcInMonument(string monumentID, NetworkableId netID) => IsNpcInMonument(monumentID, BaseNetworkable.serverEntities.Find(netID) as BasePlayer); private bool IsNpcInMonument(string monumentID, BasePlayer npcPlayer) { if (npcPlayer != null && _monumentsList.TryGetValue(monumentID, out var watcher)) return watcher.NpcsList.Contains(npcPlayer); return false; } private bool IsEntityInMonument(string monumentID, NetworkableId netID) => IsEntityInMonument(monumentID, BaseNetworkable.serverEntities.Find(netID) as BaseEntity); private bool IsEntityInMonument(string monumentID, BaseEntity entity) { if (entity != null && _monumentsList.TryGetValue(monumentID, out var watcher)) return watcher.EntitiesList.Contains(entity); return false; } private void ShowBounds(string monumentID, BasePlayer player, float duration = 20f) { if (player != null && _monumentsList.TryGetValue(monumentID, out var watcher)) ShowBounds(watcher, player, duration); } private void ShowBounds(MonumentWatcher watcher, BasePlayer player, float duration = 20f) { if (watcher != null && player != null) { bool isAdmin = player.IsAdmin; try { if (!isAdmin) UpdateFlag(player, BasePlayer.PlayerFlags.IsAdmin, true); //TEXT player.SendConsoleCommand("ddraw.text", duration, Color.magenta, watcher.transform.position, watcher.ID); //CENTER player.SendConsoleCommand("ddraw.sphere", duration, Color.green, watcher.transform.position, 1f); //CORNERS var corners = GetBoxCorners(watcher.boxCollider); foreach (var corner in corners) player.SendConsoleCommand("ddraw.sphere", duration, Color.red, corner, 1f); //LINES foreach (var kvp in GetLinesBetweenCorners(corners)) { foreach (var endPos in kvp.Value) player.SendConsoleCommand("ddraw.line", duration, Color.red, kvp.Key, endPos); } } finally { if (!isAdmin) UpdateFlag(player, BasePlayer.PlayerFlags.IsAdmin, false); } } } #endregion #region ~Oxide Hooks~ void OnEntitySpawned(CargoShip cargoShip) => CreateCargoWatcher(cargoShip); void OnEntityDeath(BasePlayer player) { if (player.userID.IsSteamId()) { if (_playersInMonuments.TryGetValue(player.userID, out var watchers)) { for (int i = watchers.Count - 1; i >= 0; i--) watchers[i]?.OnPlayerExit(player, Str_Death); } } else if (_npcsInMonuments.TryGetValue(player.net.ID, out var watchers)) { for (int i = watchers.Count - 1; i >= 0; i--) watchers[i]?.OnNpcExit(player, Str_Death); } } void OnEntityKill(BaseEntity entity) { if (entity != null && entity.net != null && _entitiesInMonuments.TryGetValue(entity.net.ID, out var watchers)) { for (int i = watchers.Count - 1; i >= 0; i--) watchers[i]?.OnEntityExit(entity, Str_Death); } } void OnPlayerTeleported(BasePlayer player, Vector3 oldPos, Vector3 newPos) { if (_playersInMonuments.TryGetValue(player.userID, out var watchers)) { MonumentWatcher watcher; for (int i = watchers.Count - 1; i >= 0; i--) { watcher = watchers[i]; if (watcher != null && watcher.IsInBounds(oldPos) && !watcher.IsInBounds(newPos)) watcher.OnPlayerExit(player, Str_Leave); } } } void Init() { Unsubscribe(nameof(OnEntitySpawned)); Unsubscribe(nameof(OnEntityDeath)); Unsubscribe(nameof(OnEntityKill)); Unsubscribe(nameof(OnPlayerTeleported)); Instance = this; permission.RegisterPermission(PERMISSION_ADMIN, this); AddCovalenceCommand(_config.Command, nameof(MonumentsWatcher_Command)); } void OnServerInitialized() { if (string.IsNullOrWhiteSpace(_config.WipeID) || _config.WipeID != SaveRestore.WipeId) { _config.WipeID = SaveRestore.WipeId; if (_config.Wipe_Recreate) { _monumentsBounds = new Hash(); SaveBoundsConfig(); PrintWarning("Wipe detected! Monument boundaries (excluding custom ones) have been reset to ensure the proper creation of new boundaries."); } SaveConfig(); } InitMonuments(); Interface.CallHook("OnMonumentsWatcherLoaded"); } #endregion #region ~Commands~ private void MonumentsWatcher_Command(IPlayer player, string command, string[] args) { if (!player.IsServer && !permission.UserHasPermission(player.Id, PERMISSION_ADMIN)) return; if (args != null && args.Length > 0) { var bPlayer = player.Object as BasePlayer; string replyKey = string.Empty; string[] replyArgs = new string[5]; bool isWarning = false; if (args[0] == "rotation") { MonumentWatcher watcher = null; float yRot = 0f; if (args.Length > 2 && float.TryParse(args[2], out yRot) && _monumentsList.TryGetValue(args[1], out watcher)) {} else if (bPlayer != null && _playersInMonuments.TryGetValue(bPlayer.userID, out var watchers) && watchers.Any()) { yRot = bPlayer.viewAngles.y; watcher = watchers[^1]; } if (watcher != null) { LoadBoundsConfig(); LoadCustomBoundsConfig(); var newRot = new Vector3(0f, yRot, 0f); if (_monumentsBounds.ContainsKey(watcher.ID)) _monumentsBounds[watcher.ID].Rotation = newRot; else if (_customMonumentsBounds.ContainsKey(watcher.ID)) _customMonumentsBounds[watcher.ID].Rotation = newRot; watcher.transform.rotation = Quaternion.Euler(newRot); if (bPlayer != null) ShowBounds(watcher, bPlayer, 30f); FreeBoundsConfig(); replyArgs[0] = GetMonumentDisplayName(watcher.ID, player.Id); replyArgs[1] = yRot.ToString(); replyKey = "MsgMonumentRotated"; } else { replyKey = "MsgMonumentRotationNotFound"; isWarning = true; } } else if (args[0] == "recreate") { ClearWatchers(); Interface.Oxide.DataFileSystem.DeleteDataFile($"{Name}/MonumentsBounds"); if (args.Length > 1 && args[1] == "all") Interface.Oxide.DataFileSystem.DeleteDataFile($"{Name}/CustomMonumentsBounds"); InitMonuments(); replyKey = "MsgMonumentRecreated"; } else if (args[0] == "show") { if (bPlayer == null) { replyKey = "MsgMonumentShowNoPlayer"; isWarning = true; } else { var monumentsList = new List(); if (args.Length > 1) { if (_monumentsList.TryGetValue(args[1], out var watcher)) monumentsList.Add(watcher); else { foreach (var watcher2 in _monumentsList.Values) { if (watcher2.LangKey == args[1] && !monumentsList.Contains(watcher2)) monumentsList.Add(watcher2); } } } else if (_playersInMonuments.TryGetValue(bPlayer.userID, out var watchers) && watchers.Any()) monumentsList.Add(watchers[^1]); if (monumentsList.Any()) { int total = monumentsList.Count; if (total > 1) { foreach (var monumentWatcher in monumentsList) ShowBounds(monumentWatcher, bPlayer, 30f); replyArgs[0] = $"{total}"; replyArgs[1] = args[1]; replyKey = "MsgMonumentShowList"; } else { ShowBounds(monumentsList[0], bPlayer, 30f); replyArgs[0] = GetMonumentDisplayName(monumentsList[0].ID, player.Id); replyArgs[1] = $"{monumentsList[0].transform.position}"; replyKey = "MsgMonumentShow"; } } else { replyKey = "MsgMonumentShowNotFound"; isWarning = true; } } } if (!string.IsNullOrWhiteSpace(replyKey)) SendMessage(player, replyKey, replyArgs, isWarning); } } #endregion #region ~Unload~ void Unload() { ClearWatchers(); Instance = null; _monumentsBounds = null; _customMonumentsBounds = null; _config = null; } #endregion #region ~MonumentBounds~ private static Hash _monumentsBounds; private static Hash _customMonumentsBounds; private const string _monumentsBoundsPath = "MonumentsWatcher/MonumentsBounds"; private const string _customMonumentsBoundsPath = "MonumentsWatcher/CustomMonumentsBounds"; private void LoadBoundsConfig() { if (Interface.Oxide.DataFileSystem.ExistsDatafile(_monumentsBoundsPath)) { try { _monumentsBounds = Interface.Oxide.DataFileSystem.ReadObject>(_monumentsBoundsPath); } catch (Exception ex) { UnityEngine.Debug.LogException(ex); } } if (_monumentsBounds == null) { _monumentsBounds = new Hash(); SaveBoundsConfig(); } } private void LoadCustomBoundsConfig() { if (Interface.Oxide.DataFileSystem.ExistsDatafile(_customMonumentsBoundsPath)) { try { _customMonumentsBounds = Interface.Oxide.DataFileSystem.ReadObject>(_customMonumentsBoundsPath); } catch (Exception ex) { UnityEngine.Debug.LogException(ex); } } if (_customMonumentsBounds == null) { _customMonumentsBounds = new Hash(); SaveCustomBoundsConfig(); } } private void FreeBoundsConfig() { _monumentsBounds.Clear(); _customMonumentsBounds.Clear(); } private void SaveBoundsConfig() => Interface.Oxide.DataFileSystem.WriteObject(_monumentsBoundsPath, _monumentsBounds); private void SaveCustomBoundsConfig() => Interface.Oxide.DataFileSystem.WriteObject(_customMonumentsBoundsPath, _customMonumentsBounds); public class MonumentBounds { public Vector3 Center { get; set; } public Vector3 CenterOffset { get; set; } public Vector3 Size { get; set; } public Vector3 Rotation { get; set; } } public class CustomMonumentBounds : MonumentBounds { public WatcherMonumentType MonumentType { get; set; } } public class BoundsValues { public Vector3 CenterOffset { get; set; } public Vector3 Size { get; set; } public BoundsValues(Vector3 offset, Vector3 size) { CenterOffset = offset; Size = size; } } #endregion #region ~MonumentWatcher~ public enum WatcherMonumentType { SafeZone, RadTown, RadTownWater, RadTownSmall, TunnelStation, MiningQuarry, BunkerEntrance, Cave, Swamp, IceLake, PowerSubstation, WaterWell, Custom } public class MonumentWatcher : MonoBehaviour { public string ID { get; private set; } private WatcherMonumentType _type; public WatcherMonumentType Type { get => _type; private set { _type = value; TypeString = value.ToString(); } } public string TypeString { get; private set; } public string Prefab { get; private set; } public string LangKey { get; set; } public string Suffix { get; private set; } public bool IsMoveable { get; private set; } public bool IsCustom { get; private set; } public Vector3 Size { get; private set; } public HashSet PlayersList = Pool.Get>(); public HashSet NpcsList = Pool.Get>(); public HashSet EntitiesList = Pool.Get>(); private Rigidbody rigidbody; public BoxCollider boxCollider; public Bounds colliderBounds; private void Awake() { gameObject.layer = (int)Layer.Reserved1; gameObject.name = "MonumentWatcher"; enabled = false; } public void InitializeProperties(string id, WatcherMonumentType type, string prefab, string langKey, string suffix = "", bool isMoveable = false, bool isCustom = false) { ID = id; Type = type; Prefab = prefab; LangKey = langKey; Suffix = suffix; IsMoveable = isMoveable; IsCustom = isCustom; } public void InitializeCargoShip(CargoShip cargoShip) { transform.parent = cargoShip.transform; transform.localPosition = Instance._cargoBounds.CenterOffset; transform.localRotation = Quaternion.identity; if (boxCollider != null) DestroyImmediate(boxCollider); if (rigidbody != null) DestroyImmediate(rigidbody); rigidbody = gameObject.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; rigidbody.detectCollisions = true; rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete; boxCollider = gameObject.GetComponent(); if (boxCollider is null) { boxCollider = gameObject.AddComponent(); boxCollider.isTrigger = true; } boxCollider.size = Instance._cargoBounds.Size; colliderBounds = boxCollider.bounds; } public void InitializeMonument(MonumentBounds bounds = null) { if (bounds != null) { transform.position = bounds.Center + (Quaternion.Euler(bounds.Rotation) * bounds.CenterOffset); transform.rotation = Quaternion.Euler(bounds.Rotation); Size = bounds.Size; } if (boxCollider != null) DestroyImmediate(boxCollider); if (rigidbody != null) DestroyImmediate(rigidbody); rigidbody = gameObject.AddComponent(); rigidbody.useGravity = false; rigidbody.isKinematic = true; rigidbody.detectCollisions = true; rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete; boxCollider = gameObject.GetComponent(); if (boxCollider is null) { boxCollider = gameObject.AddComponent(); boxCollider.isTrigger = true; } boxCollider.size = Size; colliderBounds = boxCollider.bounds; } private void OnTriggerEnter(Collider collider) { if (Instance is null || collider is null || collider.gameObject is null || collider.gameObject.ToBaseEntity() is not BaseEntity entity || !entity.IsValid()) return; bool callHook = true; string oldMonumentID = string.Empty; MonumentWatcher moveableWatcher = null; if (entity is BasePlayer player) { if (player.userID.IsSteamId()) { if (PlayersList.Add(player)) { if (!Instance._playersInMonuments.TryGetValue(player.userID, out var watchers)) Instance._playersInMonuments[player.userID] = watchers = new List(); if (watchers.Any()) oldMonumentID = watchers[^1].ID; watchers.Add(this); if (!this.IsMoveable) { foreach (var watcher in watchers) { if (watcher.IsMoveable) moveableWatcher = watcher; } if (moveableWatcher != null) { watchers.Remove(moveableWatcher); watchers.Add(moveableWatcher); callHook = false; } } if (callHook) Interface.CallHook(HooksOnPlayerEnteredMonument, ID, player, TypeString, oldMonumentID); } } else if (NpcsList.Add(player)) { if (!Instance._npcsInMonuments.TryGetValue(player.net.ID, out var watchers)) Instance._npcsInMonuments[player.net.ID] = watchers = new List(); if (watchers.Any()) oldMonumentID = watchers[^1].ID; watchers.Add(this); if (!this.IsMoveable) { foreach (var watcher in watchers) { if (watcher.IsMoveable) moveableWatcher = watcher; } if (moveableWatcher != null) { watchers.Remove(moveableWatcher); watchers.Add(moveableWatcher); callHook = false; } } if (callHook) Interface.CallHook(HooksOnNpcEnteredMonument, ID, player, TypeString, oldMonumentID); } } else if (EntitiesList.Add(entity)) { if (!Instance._entitiesInMonuments.TryGetValue(entity.net.ID, out var watchers)) Instance._entitiesInMonuments[entity.net.ID] = watchers = new List(); if (watchers.Any()) oldMonumentID = watchers[^1].ID; watchers.Add(this); if (!this.IsMoveable) { foreach (var watcher in watchers) { if (watcher.IsMoveable) moveableWatcher = watcher; } if (moveableWatcher != null) { watchers.Remove(moveableWatcher); watchers.Add(moveableWatcher); callHook = false; } } if (callHook) Interface.CallHook(HooksOnEntityEnteredMonument, ID, entity, TypeString, oldMonumentID); } } private void OnTriggerExit(Collider collider) { if (collider is null || collider.gameObject is null || collider.gameObject.ToBaseEntity() is not BaseEntity entity || !entity.IsValid()) return; if (entity is BasePlayer player) { if (player.userID.IsSteamId()) OnPlayerExit(player, Str_Leave); else OnNpcExit(player, Str_Leave); } else OnEntityExit(entity, Str_Leave); } public void OnPlayerExit(BasePlayer player, string reason, bool remove = true) { string newMonumentID = string.Empty; if (Instance._playersInMonuments.TryGetValue(player.userID, out var watchers)) { watchers.Remove(this); if (!watchers.Any()) Instance._playersInMonuments.Remove(player.userID); else if (reason != Str_Death) newMonumentID = watchers[^1].ID; } Interface.CallHook(HooksOnPlayerExitedMonument, ID, player, TypeString, reason, newMonumentID); if (remove) PlayersList.Remove(player); } public void OnNpcExit(BasePlayer player, string reason, bool remove = true) { string newMonumentID = string.Empty; if (Instance._npcsInMonuments.TryGetValue(player.net.ID, out var watchers)) { watchers.Remove(this); if (!watchers.Any()) Instance._npcsInMonuments.Remove(player.net.ID); else if (reason != Str_Death) newMonumentID = watchers[^1].ID; } Interface.CallHook(HooksOnNpcExitedMonument, ID, player, TypeString, reason, newMonumentID); if (remove) NpcsList.Remove(player); } public void OnEntityExit(BaseEntity entity, string reason, bool remove = true) { string newMonumentID = string.Empty; if (Instance._entitiesInMonuments.TryGetValue(entity.net.ID, out var watchers)) { watchers.Remove(this); if (!watchers.Any()) Instance._entitiesInMonuments.Remove(entity.net.ID); else if (reason != Str_Death) newMonumentID = watchers[^1].ID; } Interface.CallHook(HooksOnEntityExitedMonument, ID, entity, TypeString, reason, newMonumentID); if (remove) EntitiesList.Remove(entity); } public bool IsInBounds(Vector3 pos) => boxCollider.bounds.Contains(pos); private void OnDestroy() { Instance._monumentsList.Remove(ID); foreach (var player in PlayersList) { if (player != null) OnPlayerExit(player, Str_ClearList, false); } foreach (var npcPlayer in NpcsList) { if (npcPlayer != null) OnNpcExit(npcPlayer, Str_ClearList, false); } foreach (var entity in EntitiesList) { if (entity != null) OnEntityExit(entity, Str_ClearList, false); } Pool.FreeUnmanaged(ref PlayersList); Pool.FreeUnmanaged(ref NpcsList); Pool.FreeUnmanaged(ref EntitiesList); if (ID.Contains("CargoShip")) Interface.CallHook(HooksOnCargoWatcherDeleted, ID); } } #endregion } }