/* Chat Command: /wallme Command StackSize - Commands, Wood, Stone, Ice, Remove (Stack size 1 if not provided) Perms: WallMe.Wood - Required to use wood walls. WallMe.Stone - Required to use stone walls. WallMe.Adobe - Required to use adobe walls. WallMe.Frontier - Required to use frontier walls. WallMe.Ice - Required to use ice walls. WallMe.Remove - Required to use remove command. WallMe.Refund - Will give walls and gates back on remove. WallMe.Nolimit - Wont check for anything blocking wall placement. WallMe.Nocost - Wont cost walls and gates to use command. WallMe.Noownercheck - Doesnt check who owner of TC is (usful for admins). */ using Newtonsoft.Json; using Oxide.Core; using Rust; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Oxide.Plugins { [Info("WallMe", "bmgjet", "1.0.0")] [Description("Spawn Wall Around Base Via TC Command")] class WallMe : RustPlugin { private StoredData storedData; private class StoredData { public Dictionary> walls = new Dictionary>(); } #region Permissions private const string PermWood = "WallMe.Wood"; private const string PermStone = "WallMe.Stone"; private const string PermIce = "WallMe.Ice"; private const string PermAdobe = "WallMe.Adobe"; private const string PermFrontier = "WallMe.Frontier"; private const string PermRemove = "WallMe.Remove"; private const string PermRefund = "WallMe.Refund"; private const string PermNoLimit = "WallMe.Nolimit"; private const string PermNoCost = "WallMe.Nocost"; private const string PermNoOwnerCheck = "WallMe.Noownercheck"; #endregion #region Language protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { {"ClearSave", "Data File Wiped."}, {"NoPerm", "You don't have {0} perm!"}, {"InvalidArg", "Invalid Args Use /wallme command stacksize"}, {"Commands", "Avaliable Commands: {0}"}, {"Removed", "Removed {0} walls."}, {"Adding", "Adding {0} walls."}, {"NoTC", "TC Not Found, Look At TC When Using This Command."}, {"AlreadyHas", "TC already has walls registered to it."}, {"Need", "You need more resources:\n{0}"}, {"NoBuilding", "No building found!"}, {"NotOwner", "Only the owner of this TC can use this command."}, {"NotAuthed", "You must be building authed to use this command."}, {"NotFound", "TC doesn't have any walls registered with it."}, {"NotEnough", "Not enough building blocks to use this function!"}, {"BlockedBy", "Wall placement has been blocked by {0}."}, {"ToLarge", "Your base is to large to use this function!"}, {"MaxStack", "The max amount of stacks you can use is {0}."}, }, this); } //Language lookups private string message(BasePlayer player, string key, params object[] args) { if (player == null) { return string.Format(lang.GetMessage(key, this), args); } return string.Format(lang.GetMessage(key, this, player.UserIDString), args); } #endregion #region Configuration private Configuration config; private class Configuration { [JsonProperty("Only TC Owner Can Use WallMe Command")] public bool OwnerOnly = true; [JsonProperty("Must Be Authed On TC")] public bool TCAuthed = true; [JsonProperty("Remove Decay From Walls")] public bool NoWallDecay = true; [JsonProperty("Show Where Collisions Block Placement With Sphere")] public bool ShowCollisions = true; [JsonProperty("Draw Collisions Sphere Time (sec)")] public float DrawTime = 60; [JsonProperty("Collision Check Size")] public float CollisionSize = 1.25f; [JsonProperty("Max Amount Of Wall Stacks")] public int MaxStacks = 4; [JsonProperty("Max Radius Of Base Allowed")] public int MaxRadius = 48; public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonConvert.DeserializeObject>(ToJson()); } protected override void LoadDefaultConfig() { config = new Configuration(); } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) { throw new JsonException(); } if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys)) { PrintWarning("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch { PrintWarning($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() { PrintWarning($"Configuration changes saved to {Name}.json"); Config.WriteObject(config, true); } #endregion Configuration #region Oxide Hooks void Init() { storedData = Interface.Oxide.DataFileSystem.ReadObject(Name); //Add permissions permission.RegisterPermission(PermWood, this); permission.RegisterPermission(PermStone, this); permission.RegisterPermission(PermIce, this); permission.RegisterPermission(PermAdobe, this); permission.RegisterPermission(PermRemove, this); permission.RegisterPermission(PermRefund, this); permission.RegisterPermission(PermNoLimit, this); permission.RegisterPermission(PermNoCost, this); permission.RegisterPermission(PermFrontier, this); permission.RegisterPermission(PermNoOwnerCheck, this); } //New Map Save So Clear Data void OnNewSave(string filename) { Puts(message(null, "ClearSave")); storedData.walls.Clear(); } //Killed TC private void OnEntityKill(BuildingPrivlidge bn) { if (bn != null && bn.ShortPrefabName == "cupboard.tool.deployed") { //Remove from save file if (storedData.walls.ContainsKey(bn.net.ID.Value)) { RemoveWall(null, bn.net.ID.Value); } } } private void Unload() { Interface.Oxide.DataFileSystem.WriteObject(Name, storedData); } [ChatCommand("wallme")] private void wallme(BasePlayer player, string command, string[] args) { if (args.Length == 1 || args.Length == 2) //Check arg provided { BuildingPrivlidge TC = FindTC(player.eyes.HeadRay()); if (TC == null) { player.ChatMessage(message(player, "NoTC")); return; } if (config.OwnerOnly && !permission.UserHasPermission(player.UserIDString,PermNoOwnerCheck) && TC.OwnerID != player.userID) { player.ChatMessage(message(player, "NotOwner")); return; } if (config.TCAuthed && !TC.IsAuthed(player.userID)) { player.ChatMessage(message(player, "NotAuthed")); return; } int stacks = 1; if (args.Length == 2) { if (int.TryParse(args[1], out stacks)) { if (stacks > config.MaxStacks) { player.ChatMessage(message(player, "MaxStack", config.MaxStacks)); return; } } } //Command switch switch (args[0]) { case "stone": if (HasPerm(PermStone, player.UserIDString)) { player.ChatMessage(message(player, "Adding", args[0])); AddWalls(player, 1585379529, 4211374971, -967648160, -691113464, TC, stacks); return; } player.ChatMessage(message(player, "NoPerm", args[0])); return; case "wood": if (HasPerm(PermWood, player.UserIDString)) { player.ChatMessage(message(player, "Adding", args[0])); AddWalls(player, 1745077396, 95147612, 99588025, -335089230, TC, stacks); return; } player.ChatMessage(message(player, "NoPerm", args[0])); return; case "ice": if (HasPerm(PermIce, player.UserIDString)) { player.ChatMessage(message(player, "Adding", args[0])); AddWalls(player, 921229511, 4211374971, -985781766, -691113464, TC, stacks); return; } player.ChatMessage(message(player, "NoPerm", args[0])); return; case "adobe": if (HasPerm(PermAdobe, player.UserIDString)) { player.ChatMessage(message(player, "Adding", args[0])); AddWalls(player, 542454407, 1587209110, 756890702, -401905610, TC, stacks); return; } player.ChatMessage(message(player, "NoPerm", args[0])); return; //PermFrontier case "frontier": if (HasPerm(PermFrontier, player.UserIDString)) { player.ChatMessage(message(player, "Adding", args[0])); AddWalls(player, 4026344599, 798296829, 63265850, 2137338174, TC, stacks); return; } player.ChatMessage(message(player, "NoPerm", args[0])); return; case "remove": if (HasPerm(PermRemove, player.UserIDString)) { RemoveWall(player); return; } player.ChatMessage(message(player, "NoPerm", args[0])); return; } } player.ChatMessage(message(player, "InvalidArg")); player.ChatMessage(message(player, "Commands", ((HasPerm(PermWood, player.UserIDString) ? "wood " : string.Empty) + (HasPerm(PermStone, player.UserIDString) ? "stone " : string.Empty) + (HasPerm(PermIce, player.UserIDString) ? "ice " : string.Empty) + (HasPerm(PermAdobe, player.UserIDString) ? "adobe " : string.Empty) + (HasPerm(PermFrontier, player.UserIDString) ? "frontier " : string.Empty) + (HasPerm(PermRemove, player.UserIDString) ? "remove" : string.Empty)))); } #endregion #region Methods private Vector3 Center(Vector3 start, Vector3 end, float percent) { return (start + percent * (end - start)); } private bool HasPerm(string Perm, string userid) { return permission.UserHasPermission(userid, Perm) || permission.GroupHasPermission("default", Perm); } private BuildingPrivlidge FindTC(Ray ray) { RaycastHit hit; var raycast = UnityEngine.Physics.Raycast(ray, out hit, 5, -1); BaseEntity entity = raycast ? hit.GetEntity() : null; if (entity != null && entity is BuildingPrivlidge) { return entity as BuildingPrivlidge; } return null; } private void Take(BasePlayer player, int itemid, int amount) { var collect = Facepunch.Pool.Get>(); player.inventory.Take(collect, itemid, amount); // Take only calls RemoveFromContainer foreach (Item item in collect) { item.Remove(); } Facepunch.Pool.FreeUnmanaged(ref collect); } private int ValidRadius(float radius) { if (radius < 6) { radius = 12; } else if (radius >= 6 && radius < 18) { radius = 18; } else if (radius >= 18 && radius < 36) { radius = 24; } else if (radius >= 36 && radius < 48) { radius = 30; } else if (radius >= 48 && radius < 54) { radius = 36; } else if (radius >= 54 && radius < 60) { radius = 42; } else if (radius >= 60 && radius < 67) { radius = 48; } else if (radius >= 67 && radius < 100) { radius *= 0.65f; } else if (radius >= 100 && radius < 150) { radius *= 0.6f; } else if (radius >= 150 && radius < 200) { radius *= 0.55f; } else if (radius >= 200 && radius < 250) { radius *= 0.50f; } return (int)radius; } private void ShowCollision(BasePlayer player, Vector3 pos) { //Check for plugins that toggle admin to prevent fly hack kick if (player == null || !player.userID.IsSteamId() || !player.IsAlive() || player.Connection == null || (!player.IsAdmin && !player.IsDeveloper && player.IsFlying)) { return; }// BasePlayer => FinalizeTick => NoteAdminHack => Ban => Cheat Detected! //Admin toggle so can use console command bool _isadmin = player.IsAdmin; if (!_isadmin) { player.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, true); player.SendNetworkUpdateImmediate(); } //Send the ambient volumes console command player.SendConsoleCommand("ddraw.sphere", config.DrawTime, Color.blue, pos, 3f); //Untoggle admin if (!_isadmin && player.HasPlayerFlag(BasePlayer.PlayerFlags.IsAdmin)) { player.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, false); player.SendNetworkUpdateImmediate(); } } private void RemoveWall(BasePlayer player, ulong netid = 0) { //Find TC if passed but netid or players eyes BuildingPrivlidge TC = null; if (netid != 0) { TC = BaseNetworkable.serverEntities.Find(new NetworkableId(netid)) as BuildingPrivlidge; } if (TC == null && player != null) { TC = FindTC(player.eyes.HeadRay()); if (TC != null) { netid = TC.net.ID.Value; } } if (netid == 0 && TC == null) { return; } //Check Perms if (TC != null) { if (player != null && config.OwnerOnly && TC.OwnerID != player.userID) { player.ChatMessage(message(player, "NotOwner")); return; } if (player != null && config.TCAuthed && !TC.IsAuthed(player.userID)) { player.ChatMessage(message(player, "NotAuthed")); return; } } //Find in save data if (storedData.walls.ContainsKey(netid)) { //Check if should refund bool Refund = false; if (player != null && !HasPerm(PermNoCost, player.UserIDString) && HasPerm(PermRefund, player.UserIDString)) { Refund = true; } //Find all walls foreach (uint ent in storedData.walls[netid]) { try { var bn = BaseNetworkable.serverEntities.Find(new NetworkableId(ent)); if (bn != null && !bn.IsDestroyed) { //Refund correct type if (Refund) { switch (bn.ShortPrefabName) { case "wall.external.high.stone": player.GiveItem(ItemManager.CreateByItemID(-967648160)); break; case "gates.external.high.stone": player.GiveItem(ItemManager.CreateByItemID(-691113464)); break; case "wall.external.high.wood": player.GiveItem(ItemManager.CreateByItemID(99588025)); break; case "gates.external.high.wood": player.GiveItem(ItemManager.CreateByItemID(-335089230)); break; case "wall.external.high.ice": player.GiveItem(ItemManager.CreateByItemID(-985781766)); break; case "gates.external.high.adobe": player.GiveItem(ItemManager.CreateByItemID(-401905610)); break; case "wall.external.high.adobe": player.GiveItem(ItemManager.CreateByItemID(756890702)); break; case "gates.external.high.frontier": player.GiveItem(ItemManager.CreateByItemID(2137338174)); break; case "wall.external.high.frontier": player.GiveItem(ItemManager.CreateByItemID(63265850)); break; } } bn.Kill(); //Destroy wall } } catch { } } if (player != null) { player.ChatMessage(message(player, "Removed", storedData.walls[TC.net.ID.Value].Count)); } storedData.walls.Remove(TC.net.ID.Value); //Remove from save return; } if (player != null) { player.ChatMessage(message(player, "NotFound")); } } private void AddWalls(BasePlayer player, uint wallid, uint gateid, int wallitem, int gateitem, BuildingPrivlidge TC, int stacks, float radius = 0) { //Make sure doesnt already have save for this tc if (storedData.walls.ContainsKey(TC.net.ID.Value)) { player.ChatMessage(message(player, "AlreadyHas")); return; } var building = TC.GetNearbyBuildingBlock()?.GetBuilding(); if (building != null) //All building parts { List blocks = Facepunch.Pool.Get>(); //Seperate foundations since thats all we need to work with. Will make large bases process quicker foreach (var bb in building.buildingBlocks) { if (bb.ShortPrefabName.Contains("foundation")) { blocks.Add(bb); } } //Check there are enough if (blocks.Count < 2) { player.ChatMessage(message(player, "NotEnough")); return; } Vector3 edge1 = Vector3.zero; Vector3 edge2 = Vector3.zero; //Compare each building foundation against every other foundation to get the 2 furthest away from each other on 2D plane foreach (var bb in blocks) { foreach (var bb2 in blocks) { float distance = Vector3Ex.Distance2D(bb.transform.position, bb2.transform.position); if (distance > radius) { edge1 = bb.transform.position; edge2 = bb2.transform.position; radius = (int)distance; } } } Facepunch.Pool.FreeUnmanaged(ref blocks); if (edge1 != Vector3.zero && edge2 != Vector3.zero) { //Found 2 furthest foundations, Create A good radius with in building privlage radius = ValidRadius(radius); if (radius > config.MaxRadius) { player.ChatMessage(message(player, "ToLarge")); return; } //Get center var center = Center(edge2, edge1, 0.5f); //Generate list of positions List wallpos = GetPositions(center, radius, (360 / radius), stacks); //Check all spots are safe if (!HasPerm(PermNoLimit, player.UserIDString)) { foreach (var wall in wallpos) { if (GamePhysics.CheckSphere(wall, config.CollisionSize, Layers.Mask.Vehicle_Large | Layers.Mask.Construction | Layers.Mask.Vehicle_Detailed | Layers.Mask.Vehicle_World | Layers.Mask.Prevent_Building | Layers.Mask.Player_Server | Layers.Server.Deployed, QueryTriggerInteraction.Ignore)) { player.ChatMessage(message(player, "BlockedBy", "Entity")); if (config.ShowCollisions) { ShowCollision(player, wall); } return; } if (TerrainMeta.TopologyMap.GetTopology(wall, TerrainTopology.ROAD) || TerrainMeta.TopologyMap.GetTopology(wall, TerrainTopology.RAIL)) { player.ChatMessage(message(player, "BlockedBy", "Topology")); if (config.ShowCollisions) { ShowCollision(player, wall); } return; } //Check for building blocks by colliders bool Blocked = false; List list = Facepunch.Pool.Get>(); Vis.Colliders(wall, 3.5f, list, -1, QueryTriggerInteraction.Collide); using (List.Enumerator enumerator = list.GetEnumerator()) { while (enumerator.MoveNext()) { if (enumerator.Current.GetComponent() != null && enumerator.Current.name == "prevent_building") { Blocked = true; player.ChatMessage(message(player, "BlockedBy", enumerator.Current.name)); break; } if (enumerator.Current.GetComponent() != null) { Blocked = true; player.ChatMessage(message(player, "BlockedBy", "SafeZone")); break; } } } Facepunch.Pool.FreeUnmanaged(ref list); if (Blocked) { if (config.ShowCollisions) { ShowCollision(player, wall); } return; } } } //Needed Amounts if (HaveResources(player, wallpos.Count - 2, wallitem, gateitem)) { storedData.walls.Add(TC.net.ID.Value, CreateWallsfunction(center, wallpos, player, wallid, gateid, stacks)); } } return; } player.ChatMessage(message(player, "NoBuilding")); } private bool HaveResources(BasePlayer player, int walls, int wallid, int gateid) { if (HasPerm(PermNoCost, player.UserIDString)) { return true; } Dictionary Needed = new Dictionary(); //Check has needed meterials ItemDefinition wall = ItemManager.FindItemDefinition(wallid); ItemDefinition gate = ItemManager.FindItemDefinition(gateid); int wallamount = player.inventory.GetAmount(wall.itemid); int gateamount = player.inventory.GetAmount(gate.itemid); if (wallamount < walls) { if (!Needed.ContainsKey(wall.shortname)) { Needed.Add(wall.shortname, 0); } Needed[wall.shortname] += (walls - wallamount); } if (gateamount < 2) { if (!Needed.ContainsKey(gate.shortname)) { Needed.Add(gate.shortname, 0); } Needed[gate.shortname] += (2 - gateamount); } //Has everything needed so remove from player and allow if (Needed.Count == 0) { Take(player, wall.itemid, walls); Take(player, gate.itemid, 2); return true; } //Doesnt have everything needed to build list and message. else { string text = ""; foreach (var item in Needed) { text += $" * {item.Key} x{item.Value}\n"; } player.ChatMessage(message(player, "Need", text)); return false; } } private List GetPositions(Vector3 center, float radius, float next, int stacks = 1) { //Create list of positions in a circle based on wall size var positions = new List(); if (next < 1f) { next = 1f; } float angleInRadians = 2 * (float)Math.PI; for (int i = 0; i < stacks; i++) //Add extra stacks on { float angle = 0f; while (angle < 360) { float radian = (angleInRadians / 360) * angle; float x = center.x + radius * (float)Math.Cos(radian); float z = center.z + radius * (float)Math.Sin(radian); Vector3 pos = new Vector3(x, 0f, z); pos.y = TerrainMeta.HeightMap.GetHeight(pos) + (5 * i); //Correct to terrain height (Need to do somethign about cliffs/prefabs positions.Add(pos); angle += next; } } return positions; } private List CreateWallsfunction(Vector3 center, List wallpos, BasePlayer player, uint wallid, uint gateid, int stacks) { List WallsList = new List(); Quaternion quat = Quaternion.Euler(0, 180, 0); BaseEntity e; int count = 0; //Random gate positions int gate1 = UnityEngine.Random.Range(1, wallpos.Count / stacks); int gate2 = 0; for (int i = 0; i < 100; i++) { gate2 = (UnityEngine.Random.Range(0, wallpos.Count / stacks)); if (gate2 != gate1) { break; } } //Add wall parts foreach (var position in wallpos) { if (count != gate1 && count != gate2) { e = GameManager.server.CreateEntity(StringPool.Get(wallid), position, default(Quaternion), true); } else { e = GameManager.server.CreateEntity(StringPool.Get(gateid), position, default(Quaternion), true); } if (e == null) { continue; } //Set up wall/gate prefab center.y = position.y; e.transform.LookAt(center, Vector3.up); if (count != gate1 && count != gate2) { e.transform.rotation *= quat; } //Turn walls so soft side of wood walls inside e.enableSaving = true; e.SetParent(null, false, false); if (wallid == 4026344599) { e.Invoke("PopulateVariants", 0f); } e.skinID = 0; e.Spawn(); e.OwnerID = player.OwnerID; BaseCombatEntity baseCombatEntity2 = e as BaseCombatEntity; if (baseCombatEntity2 != null) { baseCombatEntity2.SetHealth(e.MaxHealth()); baseCombatEntity2.lastAttackedTime = 0; } WallsList.Add(e.net.ID.Value); e.gameObject.SetActive(true); if (config.NoWallDecay) { DecayEntity bce = e.GetComponent(); if (bce != null) { bce.decay = null; } } count++; } return WallsList; } #endregion } }