using System.Collections.Generic; using Oxide.Core; using Oxide.Core.Plugins; using Oxide.Core.Configuration; using Facepunch; using System; using System.Linq; using UnityEngine; namespace Oxide.Plugins { [Info("Handy Man", "nivex", "1.3.3")] [Description("Provides AOE repair functionality to the player. Repair is only possible where you can build.")] public class HandyMan : RustPlugin { [PluginReference] Plugin NoEscape; readonly DynamicConfigFile dataFile = Interface.Oxide.DataFileSystem.GetFile(nameof(Name)); Dictionary playerData = new Dictionary(); //player preference values Dictionary entities = new Dictionary(); bool _allowHandyManFixMessage = true; bool _allowAOERepair = true; PluginTimers RepairMessageTimer; //Timer to control HandyMan chats static int constructionMask = LayerMask.GetMask("Construction"); static int allMask = LayerMask.GetMask("Construction", "Deployed"); static float lastAttackLimit = 30f; static float privDistance = 21f; bool IsRaidBlocked(string targetId) => UseRaidBlocker && (bool)(NoEscape?.Call("IsRaidBlocked", targetId) ?? false); bool HasPerm(BasePlayer player) => permission.UserHasPermission(player.UserIDString, "handyman.use") || player.IsAdmin || player.IsDeveloper || player.net.connection.authLevel > 0; bool HasResources(BasePlayer player) => player.inventory.AllItems().Any(item => item.info.shortname == "wood" || item.info.shortname == "metal.refined" || item.info.shortname == "stones" || item.info.shortname == "metal.fragments"); private void Loaded() { permission.RegisterPermission("handyman.use", this); LoadVariables(); try { playerData = dataFile.ReadObject>(); } catch { } if (playerData == null) playerData = new Dictionary(); //Set message timer to prevent user spam RepairMessageTimer = new PluginTimers(this); RepairMessageTimer.Every(HandyManChatInterval, () => _allowHandyManFixMessage = true); } void OnHammerHit(BasePlayer player, HitInfo info) { if (!_allowAOERepair || !HasPerm(player) || IsRaidBlocked(player.UserIDString) || info.HitEntity == null || info.HitEntity.IsDestroyed) { return; } var entity = info.HitEntity as BaseCombatEntity; if (!entity) { return; } if (!playerData.ContainsKey(player.userID)) { playerData[player.userID] = DefaultHandyManOn; dataFile.WriteObject(playerData); } if (playerData[player.userID]) { if (!HasResources(player)) { SendChatMessage(player, msg("No Resources", player.UserIDString)); return; } Repair(entity, player); } } void Repair(BaseCombatEntity entity, BasePlayer player) { if (player.CanBuild()) { if (_allowHandyManFixMessage) { SendChatMessage(player, msg("Fix", player.UserIDString)); _allowHandyManFixMessage = false; } RepairAOE(entity, player); } else SendChatMessage(player, msg("NotAllowed", player.UserIDString)); } private void RepairAOE(BaseCombatEntity entity, BasePlayer player) { //Prevent infinite loop _allowAOERepair = false; //gets the position of the block we just hit var position = new OBB(entity.transform, entity.bounds).ToBounds().center; //sets up the collection for the blocks that will be affected var entities = Pool.GetList(); //gets a list of entities within a specified range of the current target Vis.Entities(position, RepairRange, entities, repairDeployables ? allMask : constructionMask); int repaired = 0; if (entities.Count == 1) { _allowAOERepair = true; Pool.FreeList(ref entities); return; } //check if we have blocks - we should always have at least 1 if (entities.Count > 0) { var resources = new Dictionary(); int lastAttacked = 0; //cycle through our block list - figure out which ones need repairing foreach (var ent in entities) { //check to see if the block has been damaged before repairing. if (ent.health < ent.MaxHealth()) { if (ent.SecondsSinceAttacked <= lastAttackLimit) { lastAttacked++; continue; } var ret = CanRepair(ent, player, entities); if (ret is KeyValuePair) { var kvp = (KeyValuePair)ret; if (!resources.ContainsKey(kvp.Key)) { resources.Add(kvp.Key, kvp.Value); } else { resources[kvp.Key] += kvp.Value; } } else if (ret is bool && (bool)ret) { if (DoRepair(ent, player)) { if (markRepairedTime > 0f && player.IsAdmin) { player.SendConsoleCommand("ddraw.text", markRepairedTime, Color.green, ent.WorldSpaceBounds().ToBounds().center, "R"); } if (++repaired > maxRepairEnts) { break; } } } } } Pool.FreeList(ref entities); if (resources.Count > 0) { if (resources.Count > 1 || (resources.Count == 1 && resources.First().Key == "High Quality Metal" && resources.First().Value > 3)) { foreach (var kvp in resources) { SendChatMessage(player, msg("Missing Resources Multiple", player.UserIDString, kvp.Key, kvp.Value)); } SendChatMessage(player, msg("Missing Resources Partial", player.UserIDString)); } else { SendChatMessage(player, msg("Missing Resources Single", player.UserIDString, resources.First().Key, resources.First().Value)); } if (repaired == 0) { _allowAOERepair = true; } } if (!_allowAOERepair) { SendChatMessage(player, repaired > 0 ? msg("IFixedEx", player.UserIDString, repaired) : msg(lastAttacked > 0 && repaired == 0 ? "CannotFixYet" : "FixDone", player.UserIDString)); } } else { SendChatMessage(player, msg("MissingFix", player.UserIDString)); } _allowAOERepair = true; } object CanRepair(BaseCombatEntity entity, BasePlayer player, List entities) { float num = entity.MaxHealth() - entity.health; float num2 = num / entity.MaxHealth(); var list = entity.RepairCost(num2); if (list != null && list.Count > 0) { foreach (var ia in list) { var items = player.inventory.FindItemsByItemID(ia.itemid); int sum = items.Sum(item => item.amount); if (sum * repairMulti < ia.amount * repairMulti) { return new KeyValuePair(ia.itemDef.displayName.english, ia.amount); } } } var privs = entities.Where(ent => ent != null && ent.net != null && !ent.IsDestroyed && ent is BuildingPrivlidge).Cast().ToList(); if (privs.Count == 0) { return true; } foreach(var priv in privs) { if (priv.Distance(entity) <= privDistance) { return !priv.AnyAuthed() || priv.IsAuthed(player); } } return false; // player.CanBuild(new OBB(entity.transform, entity.bounds)); } // BaseCombatEntity public bool DoRepair(BaseCombatEntity entity, BasePlayer player) { if (!entity.repair.enabled) { return false; } if (Interface.CallHook("OnStructureRepair", new object[] { entity, player }) != null) { return false; } if (entity.SecondsSinceAttacked <= lastAttackLimit) { entity.OnRepairFailed(player, string.Format("Unable to repair: Recently damaged. Repairable in: {0:N0}s.", lastAttackLimit - entity.SecondsSinceAttacked)); return false; } float num = entity.MaxHealth() - entity.Health(); float num2 = num / entity.MaxHealth(); if (num <= 0f || num2 <= 0f) { entity.OnRepairFailed(player, "Unable to repair: Not damaged."); return false; } var list = entity.RepairCost(num2); if (list == null || list.Count == 0) { return false; } foreach (var ia in list) { ia.amount *= repairMulti; } float num3 = list.Sum(x => x.amount); if (num3 > 0f) { float num4 = list.Min(x => Mathf.Clamp01((float)player.inventory.GetAmount(x.itemid) / x.amount)); num4 = Mathf.Min(num4, 50f / num); if (num4 <= 0f) { entity.OnRepairFailed(player, "Unable to repair: Insufficient resources."); return false; } int num5 = 0; foreach (var current in list) { int amount = Mathf.CeilToInt(num4 * current.amount); num5 += player.inventory.Take(null, current.itemid, amount); } float num7 = (float)num5 / num3; entity.health += num * num7; entity.SendNetworkUpdate(BasePlayer.NetworkQueue.Update); } else { entity.health += num; entity.SendNetworkUpdate(BasePlayer.NetworkQueue.Update); } if (entity.health >= entity.MaxHealth()) { entity.OnRepairFinished(); } else { entity.OnRepair(); } return true; } [ChatCommand("handyman")] private void ChatCommand_HandyMan(BasePlayer player, string command, string[] args) { if (!HasPerm(player)) { SendChatMessage(player, msg("NoPermission", player.UserIDString)); return; } if (!playerData.ContainsKey(player.userID)) { playerData[player.userID] = DefaultHandyManOn; dataFile.WriteObject(playerData); } if (args.Length > 0) { playerData[player.userID] = args[0].ToLower() == "on"; dataFile.WriteObject(playerData); } SendChatMessage(player, msg(playerData[player.userID] ? "Hired" : "Fired", player.UserIDString)); } [ConsoleCommand("healthcheck")] private void ConsoleCommand_HealthCheck() => Puts("HandyMan is running."); #region Config private bool Changed; private bool UseRaidBlocker; private bool DefaultHandyManOn; private int RepairRange; private int HandyManChatInterval; private float repairMulti; private bool repairDeployables; private int maxRepairEnts; private float markRepairedTime; protected override void LoadDefaultMessages() { string helpText = "HandyMan - Help - v {ver} \n" + "-----------------------------\n" + "/HandyMan - Shows your current preference for HandyMan.\n" + "/HandyMan on/off - Turns HandyMan on/off."; lang.RegisterMessages(new Dictionary { {"Hired", "HandyMan has been Hired."}, {"Fired", "HandyMan has been Fired."}, {"Fix", "You fix this one, I'll get the rest."}, {"NotAllowed", "You are not allowed to build here - I can't repair for you."}, {"IFixed", "I fixed some damage over here..."}, {"IFixedEx", "I fixed {0} constructions over here..."}, {"FixDone", "Guess I fixed them all..."}, {"MissingFix", "I'm telling you... it disappeared... I can't find anything to fix."}, {"NoPermission", "You don't have permission to use this command." }, {"Help", helpText}, {"Missing Resources Single", "Missing resources: {0} ({1}). I'll need the full amount to repair this." }, {"Missing Resources Multiple", "Missing resources: {0} ({1})" }, {"Missing Resources Partial", "I can do some repairs with a partial amount of these resources." }, {"No Resources", "You must have some resources in order to repair!" }, {"CannotFixYet", "Everything has been attacked recently and cannot be repaired yet." } }, this); } void LoadVariables() //Assigns configuration data once read { HandyManChatInterval = Convert.ToInt32(GetConfig("Settings", "Chat Interval", 30)); DefaultHandyManOn = Convert.ToBoolean(GetConfig("Settings", "Default On", true)); RepairRange = Convert.ToInt32(GetConfig("Settings", "Repair Range", 50)); UseRaidBlocker = Convert.ToBoolean(GetConfig("Settings", "Use Raid Blocker", false)); repairMulti = Convert.ToSingle(GetConfig("Settings", "Repair Cost Multiplier", 1.0f)); repairDeployables = Convert.ToBoolean(GetConfig("Settings", "Repair Deployables", false)); maxRepairEnts = Convert.ToInt32(GetConfig("Settings", "Maximum Entities To Repair", 50)); markRepairedTime = Convert.ToSingle(GetConfig("Settings", "Mark Repaired Entities For X Seconds (Admins Only)", 0f)); if (repairMulti < 1.0f) repairMulti = 1.0f; if (Changed) { SaveConfig(); Changed = false; } } /// /// Responsible for loading default configuration. /// Also creates the initial configuration file /// protected override void LoadDefaultConfig() { PrintWarning("Creating a new configuration file"); Config.Clear(); LoadVariables(); } object GetConfig(string menu, string dataValue, object defaultValue) { var data = Config[menu] as Dictionary; if (data == null) { data = new Dictionary(); Config[menu] = data; Changed = true; } object value; if (!data.TryGetValue(dataValue, out value)) { value = defaultValue; data[dataValue] = value; Changed = true; } return value; } public string msg(string key, string id = null, params object[] args) { string message = id == null ? RemoveFormatting(lang.GetMessage(key, this, id)) : lang.GetMessage(key, this, id); return args.Length > 0 ? string.Format(message, args) : message; } public string RemoveFormatting(string source) { return source.Contains(">") ? System.Text.RegularExpressions.Regex.Replace(source, "<.*?>", string.Empty) : source; } private void SendChatMessage(BasePlayer player, string msg) => player.ChatMessage($"{Title}: {msg}"); #endregion } }