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

FREE MonumentsWatcher : 0.1.12 0.1.12

Нет прав для скачивания
C#:
/*
*  < ----- 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://lone.design/vendor/iiiaka/
*      https://www.patreon.com/iiiaka
*      https://boosty.to/iiiaka
*  GitHub repository page: https://github.com/IIIaKa/MonumentsWatcher
*  
*  uMod plugin page: https://umod.org/plugins/monuments-watcher
*  uMod license: https://umod.org/plugins/monuments-watcher#license
*  
*  Codefling plugin page: https://codefling.com/plugins/monuments-watcher
*  Codefling license: https://codefling.com/plugins/monuments-watcher?tab=downloads_field_4
*  
*  Lone.Design plugin page: https://lone.design/product/monuments-watcher/
*
*  Copyright © 2024-2026 IIIaKa
*/
using HarmonyLib;
using System;
using System.IO;
using System.Linq;
using System.Globalization;
using System.Collections.Generic;
using Rust;
using Rust.Ai.Gen2;
using Facepunch;
using Newtonsoft.Json;
using UnityEngine;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
namespace Oxide.Plugins {
    [Info("Monuments Watcher", "IIIaKa", "0.1.12")]
    [Description("An API plugin which helps interact with players, NPCs, animals, and other entities inside monuments.")]
    class MonumentsWatcher : RustPlugin {
#region ~Variables~
        private static MonumentsWatcher Instance {
            get;
            set;
        }
        private bool _isReady = false;
        private Harmony _harmony;
        private const string IdForHarmony = "iiiaka.monumentswatcher", PERMISSION_ADMIN = "monumentswatcher.admin", Str_Leave = "leave", Str_Death = "death", Str_MonumentDestroyed = "monument_destroyed", Str_CargoShip = "CargoShip", Hooks_OnLoaded = "OnMonumentsWatcherLoaded", Hooks_OnCargoWatcherCreated = "OnCargoWatcherCreated", Hooks_OnCargoWatcherDeleted = "OnCargoWatcherDeleted", Hooks_OnSpawnableWatcherCreated = "OnSpawnableWatcherCreated", Hooks_OnSpawnableWatcherDeleted = "OnSpawnableWatcherDeleted", Hooks_OnPlayerEnteredMonument = "OnPlayerEnteredMonument", Hooks_OnNpcEnteredMonument = "OnNpcEnteredMonument", Hooks_OnAnimalEnteredMonument = "OnAnimalEnteredMonument", Hooks_OnEntityEnteredMonument = "OnEntityEnteredMonument", Hooks_OnPlayerExitedMonument = "OnPlayerExitedMonument", Hooks_OnNpcExitedMonument = "OnNpcExitedMonument", Hooks_OnAnimalExitedMonument = "OnAnimalExitedMonument", Hooks_OnEntityExitedMonument = "OnEntityExitedMonument";
        private Hash<string, MonumentWatcher> _monumentsList = new Hash<string, MonumentWatcher>();
        private static Hash<ulong, List<MonumentWatcher>> _playersInMonuments, _npcsInMonuments, _animalsInMonuments, _entitiesInMonuments;
        private readonly string[] _defaultHooks = new string[] {
            "OnEntitySpawned", "OnEntityDeath", "OnEntityKill", "OnPlayerTeleported"
        }
        ;
#endregion
#region ~Configuration~
        private static Configuration _config;
        private class Configuration {
            [JsonProperty(PropertyName = "Chat command")]
            public string Command = string.Empty;
            [JsonProperty(PropertyName = "Is it worth enabling GameTips for messages?")]
            public bool GameTips_Enabled = true;
            [JsonProperty(PropertyName = "List of language keys for creating language files")]
            public List<string> LanguageKeys;
            [JsonProperty(PropertyName = "Is it worth recreating boundaries(excluding custom monuments) upon detecting a wipe?")]
            public bool RecreateOnWipe = true;
            [JsonProperty(PropertyName = "List of tracked categories of monuments. Leave blank to track all")]
            public HashSet<MonumentCategory> TrackedCategories;
            [JsonProperty(PropertyName = "Wipe ID")]
            public string WipeID = string.Empty;
            public Oxide.Core.VersionNumber Version;
        }
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                _config = Config.ReadObject<Configuration>();
            }
            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) {
                PrintWarning($"Your configuration file version({_config.Version}) is outdated. Updating it to {Version}...");
                string cfgPath = $"{Interface.Oxide.ConfigDirectory}{Path.DirectorySeparatorChar}{Name}.json";
                if (File.Exists(cfgPath)) File.Move(cfgPath, $"{Interface.Oxide.ConfigDirectory}{Path.DirectorySeparatorChar}_old_{Name}({_config.Version}).json");
                _config.Version = Version;
                PrintWarning($"The configuration file has been successfully updated to version {_config.Version}!");
            }
            if (string.IsNullOrWhiteSpace(_config.Command)) _config.Command = "monument";
            string langKey;
            var oldLangKeys = _config.LanguageKeys ?? new List<string>();
            _config.LanguageKeys = new List<string>() {
                "en"
            }
            ;
            for (int i = 0; i < oldLangKeys.Count; i++) {
                langKey = ToLangKey(oldLangKeys[i]);
                if (langKey != "ru" && !_config.LanguageKeys.Contains(langKey)) _config.LanguageKeys.Add(langKey);
            }
            _config.TrackedCategories ??= new HashSet<MonumentCategory>();
            SaveConfig();
        }
        protected override void SaveConfig() => Config.WriteObject(_config);
        protected override void LoadDefaultConfig() => _config = new Configuration() {
            Version = Version
        }
        ;
#endregion
#region ~Language~
        private Dictionary<string, string> _enLang = new Dictionary<string, string> {
            ["CmdMain"]
            = string.Join("\n", new string[] {
                "Available commands:\n", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>show *monumentID*(optional) *floatValue*(optional)</color> - Display the boundary of the monument you are in or specified. The display will last for the specified time or 30 seconds", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>list</color> - List of available monuments", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>rotate *monumentID*(optional) *floatValue*(optional)</color> - Rotate the monument you are in or specified, either in the direction you are looking or in the specified direction", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>recreate custom/all(optional)</color> - Recreate the boundaries of vanilla/custom/all monuments", "\n<color=#D1CBCB>Note:</color> Instead of a monument ID, you can leave it empty, but you must be inside a monument. You can also use the word 'closest' to select the nearest monument to you", "\n--------------------------------------------------"
            }
            ), ["CmdMainShowNotFound"] = "Monument not found! You must be inside a monument, or specify the name or ID of the monument", ["CmdMainShow"] = "Monument '{0}' is located at coordinates: {1}", ["CmdMainShowList"] = "{1} monuments found and displayed for the key '{0}'", ["CmdMainList"] = string.Join("\n", new string[] {
                "List of available monuments:\n", "{0}", "\n--------------------------------------------------"
            }
            ), ["CmdMainRotateNotFound"] = "You must be inside a monument and looking in the correct direction, or specify the name or ID of the monument along with the Y-coordinate for direction", ["CmdMainRotated"] = "Successful rotation of the {0} by Y-coordinate({1})!", ["CmdMainRecreated"] = "The boundaries of the monuments have been successfully recreated!", [Str_CargoShip] = "CargoShip", ["airfield_1"] = "Airfield", ["airfield_1_station"] = "Airfield Station", ["apartments_complex_1"] = "Apartments Complex", ["apartments_complex_1_station"] = "Apartments Complex 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", ["deepsea_floatingcity1"] = "Floating City", ["deepsea_floatingcity2"] = "Floating City", ["deepsea_floatingcity3"] = "Floating City", ["deepsea_floatingcity4"] = "Floating City", ["deepsea_island_tropical1"] = "Tropical Island", ["deepsea_island_tropical2"] = "Tropical Island", ["deepsea_island_tropical3"] = "Tropical Island", ["deepsea_island_tropical4"] = "Tropical Island", ["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", ["ghostship"] = "Ghost Ship", ["ghostship_b"] = "Ghost Ship", ["ghostship_c"] = "Ghost Ship", ["ghostship_d"] = "Ghost Ship", ["harbor_1"] = "Large Harbor", ["harbor_1_station"] = "Large Harbor Station", ["harbor_2"] = "Small Harbor", ["harbor_2_station"] = "Harbor Station", ["jungle_ziggurat_a"] = "Ziggurat", ["jungle_ziggurat_a_station"] = "Ziggurat 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"] = "Rad Town", ["radtown_1_station"] = "Rad Town 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", ["jungle_ruins_a"] = "Jungle Ruins", ["jungle_ruins_b"] = "Jungle Ruins", ["jungle_ruins_c"] = "Jungle Ruins", ["jungle_ruins_d"] = "Jungle Ruins", ["jungle_ruins_e"] = "Jungle Ruins", ["tropical_island_dwelling_ruins_b"] = "Tropical Ruins", ["tropical_island_dwelling_ruins_c"] = "Tropical Ruins", ["tropical_island_dwelling_ruins_d"] = "Tropical Ruins", ["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<string, string> _ruLang = new Dictionary<string, string> {
            ["CmdMain"]
            = string.Join("\n", new string[] {
                "Доступные команды:\n", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>show *айдиМонумента*(опционально) *дробноеЗначение*(опционально)</color> - Отобразить границу монумента, в котором вы находитесь или указали. Отображение будет в течении указанного времени или 30 секунд", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>list</color> - Список доступных монументов", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>rotate *айдиМонумента*(опционально) *дробноеЗначение*(опционально)</color> - Повернуть монумент, в котором вы находитесь или указали, в направлении вашего взгляда либо в указанном направлении", "<color=#D1CBCB>/monument</color> <color=#D1AB9A>recreate custom/all(опционально)</color> - Пересоздать границы ванильных/кастомных/всех монументов", "\n<color=#D1CBCB>Примечание:</color> Вместо айди монумента вы можете ничего не указывать, но должны находиться внутри монумента. Также можно указать слово 'closest', чтобы выбрать ближайший к вам монумент", "\n--------------------------------------------------"
            }
            ), ["CmdMainShowNotFound"] = "Монумент не найден! Вы должны находиться в монументе либо указать имя или ID монумента", ["CmdMainShow"] = "Монумент '{0}' расположен по координатам: {1}", ["CmdMainShowList"] = "По ключу '{0}' найдено и отображено {1} монументов", ["CmdMainList"] = string.Join("\n", new string[] {
                "Список доступных монументов:\n", "{0}", "\n--------------------------------------------------"
            }
            ), ["CmdMainRotateNotFound"] = "Вы должны находиться в монументе и смотреть в нужном направлении, либо указать имя или ID монумента и Y-координату для направления", ["CmdMainRotated"] = "Успешный поворот у {0} по Y координате({1})!", ["CmdMainRecreated"] = "Границы монументов успешно пересозданы!", [Str_CargoShip] = "Грузовой корабль", ["airfield_1"] = "Аэропорт", ["airfield_1_station"] = "Станция Аэропорт", ["apartments_complex_1"] = "Жилой комплекс", ["apartments_complex_1_station"] = "Станция Комплекс", ["arctic_research_base_a"] = "Арктическая база", ["arctic_research_base_a_station"] = "Станция Арктическая", ["bandit_town"] = "Лагерь бандитов", ["bandit_town_station"] = "Станция бандитов", ["compound"] = "Город", ["compound_station"] = "Станция Город", ["deepsea_floatingcity1"] = "Плавающий город", ["deepsea_floatingcity2"] = "Плавающий город", ["deepsea_floatingcity3"] = "Плавающий город", ["deepsea_floatingcity4"] = "Плавающий город", ["deepsea_island_tropical1"] = "Тропический остров", ["deepsea_island_tropical2"] = "Тропический остров", ["deepsea_island_tropical3"] = "Тропический остров", ["deepsea_island_tropical4"] = "Тропический остров", ["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"] = "Заправка", ["ghostship"] = "Корабль призрак", ["ghostship_b"] = "Корабль призрак", ["ghostship_c"] = "Корабль призрак", ["ghostship_d"] = "Корабль призрак", ["harbor_1"] = "Большой порт", ["harbor_1_station"] = "Станция Моряков", ["harbor_2"] = "Порт", ["harbor_2_station"] = "Станция Моряков", ["jungle_ziggurat_a"] = "Зиккурат", ["jungle_ziggurat_a_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"] = "Большая подстанция", ["jungle_ruins_a"] = "Руины", ["jungle_ruins_b"] = "Руины", ["jungle_ruins_c"] = "Руины", ["jungle_ruins_d"] = "Руины", ["jungle_ruins_e"] = "Руины", ["tropical_island_dwelling_ruins_b"] = "Тропические руины", ["tropical_island_dwelling_ruins_c"] = "Тропические руины", ["tropical_island_dwelling_ruins_d"] = "Тропические руины", ["water_well_a"] = "Колодец с водой", ["water_well_b"] = "Колодец с водой", ["water_well_c"] = "Колодец с водой", ["water_well_d"] = "Колодец с водой", ["water_well_e"] = "Колодец с водой"
        }
        ;
#endregion
#region ~Methods~
        private System.Collections.IEnumerator InitMonuments() {
            LoadDefaultBounds();
            LoadBoundsConfig(_monumentsBoundsPath, out _monumentsBounds);
            LoadBoundsConfig(_customMonumentsBoundsPath, out _customMonumentsBounds);
            ClearWatchers();
            yield return null;
            string monumentKey, prefab;
            var customIds = new Dictionary<MonumentInfo, string>();
            foreach (var kvp in World.SpawnedPrefabs) {
                foreach (var go in kvp.Value) {
                    if (go == null || !go.name.EndsWith("monument_marker.prefab") || !go.TryGetComponent(out MonumentInfo monument)) continue;
                    monumentKey = System.Text.RegularExpressions.Regex.Replace(kvp.Key.Replace(" ", "_"), @"[^\w\d_]", string.Empty).ToLowerInvariant();
                    if (!string.IsNullOrWhiteSpace(monumentKey)) customIds[monument] = monumentKey;
                }
            }
            yield return null;
            foreach (var entity in BaseNetworkable.serverEntities) {
                if (!entity.IsValid()) continue;
                if (entity is CargoShip cargoShip) CreateCargoWatcher(cargoShip);
                else if (entity is DeepSeaFloatingCity floatingCity) CreateSpawnableWatcher(floatingCity, MonumentCategory.SafeZone);
                else if (entity is DeepSeaIsland deepSeaIsland) CreateSpawnableWatcher(deepSeaIsland, MonumentCategory.DeepSeaIsland);
                else if (entity is Prefabs.Misc.GhostShip ghostShip) CreateSpawnableWatcher(ghostShip, MonumentCategory.RadTownWater);
                else if (entity is NPCDwelling npcDwelling) {
                    if (npcDwelling.PrefabName.Contains("tropical_island_dwelling_ruins_", StringComparison.OrdinalIgnoreCase)) CreateSpawnableWatcher(npcDwelling, MonumentCategory.Ruins);
                }
            }
            yield return null;
            int miningoutpost = 0, lighthouse = 0, gasstation = 0, supermarket = 0, tunnel = 0, bunker = 0, cave = 0, icelake = 0, power = 0, waterwell = 0;
            foreach (var monument in TerrainMeta.Path.Monuments) {
                prefab = monument.name.ToLower();
                if (prefab.EndsWith("monument_marker.prefab")) {
                    if (customIds.TryGetValue(monument, out monumentKey)) CreateCustomWatcher(monumentKey, monument.transform, prefab, monumentKey);
                    continue;
                }
                monumentKey = ClearMonumentName(prefab);
                if (!_defaultBoundsValues.ContainsKey(monumentKey)) continue;
                if (monument.IsSafeZone || monumentKey == "apartments_complex_1") {
                    CreateWatcher(monumentKey, MonumentCategory.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, MonumentCategory.RadTownWater, monument.transform, prefab);
                    break;
                    case "lighthouse" : lighthouse++;
                    CreateWatcher(monumentKey, MonumentCategory.RadTownSmall, monument.transform, prefab, $"_{lighthouse}", $"#{lighthouse}");
                    break;
                    case "gas_station_1" : gasstation++;
                    CreateWatcher(monumentKey, MonumentCategory.RadTownSmall, monument.transform, prefab, $"_{gasstation}", $"#{gasstation}");
                    break;
                    case "supermarket_1" : supermarket++;
                    CreateWatcher(monumentKey, MonumentCategory.RadTownSmall, monument.transform, prefab, $"_{supermarket}", $"#{supermarket}");
                    break;
                    case "warehouse" : miningoutpost++;
                    CreateWatcher(monumentKey, MonumentCategory.RadTownSmall, monument.transform, prefab, $"_{miningoutpost}", $"#{miningoutpost}");
                    break;
                    case "mining_quarry_a" : case "mining_quarry_b" : case "mining_quarry_c" : CreateWatcher(monumentKey, MonumentCategory.MiningQuarry, monument.transform, prefab);
                    break;
                    case "swamp_a" : case "swamp_b" : case "swamp_c" : CreateWatcher(monumentKey, MonumentCategory.Swamp, monument.transform, prefab);
                    break;
                    case "entrance_bunker_a" : case "entrance_bunker_b" : case "entrance_bunker_c" : case "entrance_bunker_d" : bunker++;
                    CreateWatcher(monumentKey, MonumentCategory.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, MonumentCategory.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, MonumentCategory.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, MonumentCategory.PowerSubstation, monument.transform, prefab, $"_{power}", $"#{power}");
                    break;
                    case "jungle_ruins_a" : case "jungle_ruins_b" : case "jungle_ruins_c" : case "jungle_ruins_d" : case "jungle_ruins_e" : CreateWatcher(monumentKey, MonumentCategory.Ruins, monument.transform, prefab);
                    break;
                    case "water_well_a" : case "water_well_b" : case "water_well_c" : case "water_well_d" : case "water_well_e" : waterwell++;
                    CreateWatcher(monumentKey, MonumentCategory.WaterWell, monument.transform, prefab, $"_{waterwell}", $"#{waterwell}");
                    break;
                    default : CreateWatcher(monumentKey, MonumentCategory.RadTown, monument.transform, prefab);
                    break;
                }
            }
            yield return null;
            Vector3 groundPos;
            float stationDistance = 100f;
            MonumentWatcher parentWatcher;
            foreach (var station in TerrainMeta.Path.DungeonGridCells) {
                if (!station.name.Contains("/tunnel-station/station-", StringComparison.OrdinalIgnoreCase)) continue;
                parentWatcher = null;
                prefab = station.name.ToLower();
                monumentKey = ClearMonumentName(prefab);
                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.Category == MonumentCategory.SafeZone || monument.Category == MonumentCategory.RadTown) && Vector3.Distance(monument.boxCollider.ClosestPointOnBounds(groundPos), groundPos) <= stationDistance) parentWatcher = monument;
                }
                if (parentWatcher != null) {
                    CreateWatcher(monumentKey, MonumentCategory.TunnelStation, station.transform, prefab, $"_{parentWatcher.ID}");
                    if (_monumentsList.TryGetValue($"{monumentKey}_{parentWatcher.ID}", out var stationWatcher)) stationWatcher.TextKey = $"{parentWatcher.ID}_station";
                    continue;
                }
                tunnel++;
                CreateWatcher(monumentKey, MonumentCategory.TunnelStation, station.transform, prefab, $"_{tunnel}", $"#{tunnel}");
            }
            yield return null;
            SaveBoundsConfig(_defaultBoundsPath, _defaultBoundsValues);
            SaveBoundsConfig(_monumentsBoundsPath, _monumentsBounds);
            SaveBoundsConfig(_customMonumentsBoundsPath, _customMonumentsBounds);
            ClearBoundsConfig();
            for (int i = 0; i < _config.LanguageKeys.Count; i++) HandleLanguageFile(_enLang, _config.LanguageKeys[i]);
            HandleLanguageFile(_ruLang, "ru");
            _enLang.Clear();
            _ruLang.Clear();
            _harmony = new Harmony(IdForHarmony);
            _harmony.Patch(AccessTools.Method(typeof(ConVar.DeepSea), "enterdeepsea"), postfix : new HarmonyMethod(typeof(MonumentsWatcher), nameof(ModEnterDeepSea), new Type[] {
                typeof(ConsoleSystem.Arg)
            }
            ));
            _harmony.Patch(AccessTools.Method(typeof(ConVar.DeepSea), "leavedeepsea"), postfix : new HarmonyMethod(typeof(MonumentsWatcher), nameof(ModLeaveDeepSea), new Type[] {
                typeof(ConsoleSystem.Arg)
            }
            ));
            for (int i = 0; i < _defaultHooks.Length; i++) Subscribe(_defaultHooks[i]);
            yield return null;
            _isReady = true;
            Interface.CallHook(Hooks_OnLoaded, Version);
        }
        private void CreateCargoWatcher(CargoShip cargoShip) {
            if (!IsTrackedCategory(MonumentCategory.RadTownWater) || !cargoShip.IsValid()) return;
            ulong cargoID = cargoShip.net.ID.Value;
            string monumentID = $"CargoShip_{cargoID}";
            var bounds = _spawnableBoundsValues[Str_CargoShip];
            var watcher = new GameObject().gameObject.AddComponent<MonumentWatcher>();
            watcher.InitializeProperties(monumentID, MonumentCategory.RadTownWater, cargoShip.name, Str_CargoShip, $"#{cargoID}", parnetID : cargoID);
            watcher.InitializeBounds(bounds.CenterOffset, bounds.Size, Quaternion.identity, cargoShip.transform);
            _monumentsList[monumentID] = watcher;
            Interface.CallHook(Hooks_OnCargoWatcherCreated, monumentID, watcher.CategoryString, cargoShip);
        }
        private void CreateSpawnableWatcher(BaseEntity entity, MonumentCategory category) {
            if (!IsTrackedCategory(category) || !entity.IsValid()) return;
            ulong netID = entity.net.ID.Value;
            string prefab = entity.name.ToLower(), monumentKey = ClearMonumentName(prefab), monumentID = $"{monumentKey}_{netID}";
            var bounds = _spawnableBoundsValues[monumentKey];
            var watcher = new GameObject().gameObject.AddComponent<MonumentWatcher>();
            watcher.InitializeProperties(monumentID, category, prefab, monumentKey, $"#{netID}", parnetID : netID);
            watcher.InitializeBounds(entity.transform.position + (entity.transform.rotation * bounds.CenterOffset), bounds.Size, entity.transform.rotation);
            _monumentsList[monumentID] = watcher;
            Interface.CallHook(Hooks_OnSpawnableWatcherCreated, monumentID, watcher.CategoryString, entity);
        }
        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) || bounds == null) {
                Collider collider;
                var colArray = new Collider[5];
                var rotation = transform.rotation.eulerAngles;
                for (var i = 0; i < Physics.OverlapSphereNonAlloc(transform.position, 1f, colArray, Rust.Layers.Mask.Prevent_Building, QueryTriggerInteraction.Ignore);
                i++) {
                    collider = colArray[i];
                    if (collider != null && collider.name.Contains("prevent_building", StringComparison.OrdinalIgnoreCase)) {
                        rotation = collider.transform.rotation.eulerAngles;
                        break;
                    }
                }
                _customMonumentsBounds[monumentID] = bounds = new CustomMonumentBounds(_defaultBoundsValues["monument_marker"], transform.position, rotation, MonumentCategory.Custom);
            }
            if (bounds.MonumentCategory != MonumentCategory.Custom && !IsTrackedCategory(bounds.MonumentCategory)) return;
            var watcher = new GameObject().AddComponent<MonumentWatcher>();
            watcher.InitializeProperties(monumentID, bounds.MonumentCategory, prefab, $"custom_{monumentID}", isCustom : true);
            watcher.InitializeBounds(bounds.Center + (Quaternion.Euler(bounds.Rotation) * bounds.CenterOffset), bounds.Size, Quaternion.Euler(bounds.Rotation));
            _monumentsList[monumentID] = watcher;
        }
        private void CreateWatcher(string monumentKey, MonumentCategory category, Transform transform, string prefab, string idSuffix = "", string suffix = "") {
            if (!IsTrackedCategory(category)) return;
            string monumentID = $"{monumentKey}{(!string.IsNullOrWhiteSpace(idSuffix) ? idSuffix : string.Empty)}";
            if (!_monumentsBounds.TryGetValue(monumentID, out var bounds) || bounds == null) _monumentsBounds[monumentID] = bounds = new MonumentBounds(_defaultBoundsValues[monumentKey], transform.position, transform.rotation.eulerAngles);
            var watcher = new GameObject().AddComponent<MonumentWatcher>();
            watcher.InitializeProperties(monumentID, category, prefab, monumentKey, suffix);
            watcher.InitializeBounds(bounds.Center + (Quaternion.Euler(bounds.Rotation) * bounds.CenterOffset), bounds.Size, Quaternion.Euler(bounds.Rotation));
            _monumentsList[monumentID] = watcher;
        }
        private void ClearWatchers() {
            for (int i = 0; i < _defaultHooks.Length; i++) Unsubscribe(_defaultHooks[i]);
            foreach (var watcher in _monumentsList.Values.ToArray()) UnityEngine.Object.Destroy(watcher.gameObject);
            _monumentsList.Clear();
            _playersInMonuments.Clear();
            _npcsInMonuments.Clear();
            _animalsInMonuments.Clear();
            _entitiesInMonuments.Clear();
        }
        private void TryDeleteWatcher(ulong parentID) {
            foreach (var watcher in _monumentsList.Values) {
                if (watcher.ParentID == parentID) {
                    UnityEngine.Object.Destroy(watcher.gameObject);
                    return;
                }
            }
        }
        private bool TryGetPlayerWatcher(BasePlayer player, out MonumentWatcher result, bool closest = true) {
            result = null;
            if (_playersInMonuments.TryGetValue(player.userID, out var watchers) && watchers.Count > 0) result = watchers[^1];
            else if (closest) {
                float minDistance = float.MaxValue, distance;
                var pos = player.transform.position;
                foreach (var watcher in _monumentsList.Values) {
                    distance = (pos - watcher.transform.position).sqrMagnitude;
                    if (distance < minDistance) {
                        minDistance = distance;
                        result = watcher;
                    }
                }
            }
            return result != null;
        }
        private bool IsTrackedCategory(MonumentCategory category) => _config.TrackedCategories.Count < 1 || _config.TrackedCategories.Contains(category);
        private void ShowBounds(MonumentWatcher watcher, BasePlayer player, float duration = 20f) {
            if (watcher == null || player == null) return;
            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
                Vector3 size = watcher.boxCollider.size * 0.5f, center = watcher.boxCollider.center, corner, startPos, endPos;
                var transform = watcher.boxCollider.transform;
                Vector3[] corners = new Vector3[8], offsets = new Vector3[8] {
                    new (-size.x, -size.y, -size.z), new (size.x, -size.y, -size.z), new (size.x, -size.y, size.z), new (-size.x, -size.y, size.z), new (-size.x, size.y, -size.z), new (size.x, size.y, -size.z), new (size.x, size.y, size.z), new (-size.x, size.y, size.z)
                }
                ;
                for (int i = 0; i < offsets.Length; i++) {
                    corner = corners[i] = transform.TransformPoint(center + offsets[i]);
                    player.SendConsoleCommand("ddraw.sphere", duration, Color.red, corner, 1f);
                }
                //LINES
                for (int i = 0; i < corners.Length; i++) {
                    startPos = corners[i];
                    for (int j = 0; j < corners.Length; j++) {
                        endPos = corners[j];
                        if (endPos != startPos) player.SendConsoleCommand("ddraw.line", duration, Color.red, startPos, endPos);
                    }
                }
                player.AddPingAtLocation(BasePlayer.PingType.GoTo, watcher.transform.position + watcher.transform.up * 2.5f, duration, new NetworkableId());
            }
            catch {
            }
            finally {
                if (!isAdmin) UpdateFlag(player, BasePlayer.PlayerFlags.IsAdmin, false);
            }
        }
        private void UpdateFlag(BasePlayer player, BasePlayer.PlayerFlags flag, bool addFlag) {
            if (player == null) return;
            player.SetPlayerFlag(flag, addFlag);
            player.SendNetworkUpdateImmediate();
        }
        private static string ClearMonumentName(string prefabName) => prefabName.Split('/')[^1].Replace(".prefab", string.Empty);
        private static void SendMessage(IPlayer player, string message, bool isWarning = true) {
            if (_config.GameTips_Enabled && !player.IsServer) player.Command("gametip.showtoast", (int)(isWarning ? GameTip.Styles.Error : GameTip.Styles.Blue_Long), message, string.Empty);
            else player.Reply(message);
        }
        private static string ToLangKey(string input) {
            if (string.IsNullOrWhiteSpace(input)) return "en";
            int inputLength = input.Length;
            if (inputLength != 2 && inputLength != 5) return "en";
            string langKey;
            for (int i = 0; i < Translate.allServerLanguages.Count; i++) {
                langKey = Translate.allServerLanguages[i];
                if (langKey.Length != inputLength) continue;
                int j;
                for (j = 0; j < inputLength; j++) {
                    if (langKey[j] != input[j] && char.ToUpperInvariant(langKey[j]) != char.ToUpperInvariant(input[j])) break;
                }
                if (j == inputLength) return langKey;
            }
            return "en";
        }
        private void HandleLanguageFile(Dictionary<string, string> langFile, string langKey) {
            var existFile = lang.GetMessages(langKey, this);
            if (existFile == null || existFile.Count < 1) {
                if (!Directory.Exists(Path.Combine(Interface.Oxide.LangDirectory, langKey))) Directory.CreateDirectory(Path.Combine(Interface.Oxide.LangDirectory, langKey));
                File.WriteAllText(Path.Combine(Interface.Oxide.LangDirectory, $"{langKey}{Path.DirectorySeparatorChar}{Name}.json"), JsonConvert.SerializeObject(langFile, Formatting.Indented));
            }
            lang.RegisterMessages(langFile, this, langKey);
        }
#endregion
#region ~API~
        private object IsReady() => _isReady ? true : null;
        private string[] GetAllMonuments() => _isReady ? _monumentsList.Keys.ToArray() : null;
        private object GetAllMonumentsNoAlloc(List<string> list) {
            if (_isReady && _monumentsList.Count > 0) {
                list.AddRange(_monumentsList.Keys);
                return true;
            }
            return null;
        }
        private Dictionary<string, string> GetAllMonumentsWithCategories() => _isReady ? _monumentsList.ToDictionary(watcher => watcher.Key, watcher => watcher.Value.CategoryString) : null;
        private object GetAllMonumentsWithCategoriesNoAlloc(Dictionary<string, string> values) {
            if (!_isReady || _monumentsList.Count < 1) return null;
            foreach (var watcher in _monumentsList.Values) values[watcher.ID] = watcher.CategoryString;
            return true;
        }
        private string[] GetMonumentsByCategory(string category) => _isReady ? _monumentsList.Where(watcher => watcher.Value.CategoryString.Equals(category, StringComparison.OrdinalIgnoreCase)).Select(watcher => watcher.Key).ToArray() : null;
        private object GetMonumentsByCategoryNoAlloc(List<string> list, string category) {
            if (!_isReady) return null;
            foreach (var watcher in _monumentsList.Values) {
                if (watcher.CategoryString.Equals(category, StringComparison.OrdinalIgnoreCase)) list.Add(watcher.ID);
            }
            return list.Count > 0 ? true : null;
        }
        private string GetMonumentCategory(string monumentID) => _isReady && _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.CategoryString : string.Empty;
        private string GetMonumentDisplayName(string monumentID, object obj, bool showSuffix = true) => GetMonumentDisplayName(monumentID, $"{obj}", showSuffix);
        private string GetMonumentDisplayName(string monumentID, ulong userID, bool showSuffix = true) => GetMonumentDisplayName(monumentID, $"{userID}", showSuffix);
        private string GetMonumentDisplayName(string monumentID, BasePlayer player, bool showSuffix = true) => GetMonumentDisplayName(monumentID, player.UserIDString, showSuffix);
        private string GetMonumentDisplayName(string monumentID, IPlayer player, bool showSuffix = true) => GetMonumentDisplayName(monumentID, player.Id, showSuffix);
        private string GetMonumentDisplayName(string monumentID, string userID = "", bool showSuffix = true) => GetMonumentDisplayNameByLang(monumentID, lang.GetLanguage(userID), showSuffix);
        private string GetMonumentDisplayNameByLang(string monumentID, string langKey = "en", bool showSuffix = true) {
            if (_isReady && _monumentsList.TryGetValue(monumentID, out var watcher)) return $"{lang.GetMessageByLanguage(watcher.TextKey, this, langKey)}{(showSuffix && !string.IsNullOrWhiteSpace(watcher.Suffix) ? $" {
                watcher.Suffix
            }
            " : string.Empty)}";
            return string.Empty;
        }
        private Vector3 GetMonumentPosition(string monumentID) => _isReady && _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.transform.position : Vector3.zero;
        private string GetMonumentByPos(Vector3 pos) {
            if (!_isReady) return null;
            foreach (var watcher in _monumentsList.Values) {
                if (watcher.IsInBounds(pos)) return watcher.ID;
            }
            return string.Empty;
        }
        private object GetMonumentsByPos(Vector3 pos) {
            if (!_isReady) return null;
            var result = new List<string>();
            foreach (var watcher in _monumentsList.Values) {
                if (watcher.IsInBounds(pos)) result.Add(watcher.ID);
            }
            if (result.Count > 0) return result.ToArray();
            return null;
        }
        private object GetMonumentsByPosNoAlloc(List<string> list, Vector3 pos) {
            if (!_isReady) return null;
            foreach (var watcher in _monumentsList.Values) {
                if (watcher.IsInBounds(pos)) list.Add(watcher.ID);
            }
            return list.Count > 0 ? true : null;
        }
        private string GetClosestMonument(Vector3 pos) {
            if (!_isReady) return null;
            MonumentWatcher result = null;
            float minDistance = float.MaxValue, distance;
            foreach (var watcher in _monumentsList.Values) {
                distance = (pos - watcher.transform.position).sqrMagnitude;
                if (distance < minDistance) {
                    minDistance = distance;
                    result = watcher;
                }
            }
            return result?.ID ?? string.Empty;
        }
        private bool IsPosInMonument(string monumentID, Vector3 pos) {
            if (_isReady && _monumentsList.TryGetValue(monumentID, out var watcher)) return watcher.IsInBounds(pos);
            return false;
        }
        private void ShowBounds(string monumentID, BasePlayer player, float duration = 20f) {
            if (_isReady && player != null && _monumentsList.TryGetValue(monumentID, out var watcher)) ShowBounds(watcher, player, duration);
        }
#endregion
#region ~API - Players~
        private object GetMonumentPlayers(string monumentID) => _isReady && _monumentsList.TryGetValue(monumentID, out var watcher) ? watcher.PlayersList.ToArray() : null;
        private object GetMonumentPlayersNoAlloc(List<BasePlayer> list, string monumentID) {
            if (_isReady && _monumentsList.TryGetValue(monumentID, out var watcher) && watcher.PlayersList.Count > 0) {
                list.AddRange(watcher.PlayersList);
              
// ... [Code preview truncated due to length. Full source in attached file] ...

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