using Rust;
using System;
using Oxide.Core;
using UnityEngine;
using System.Linq;
using Newtonsoft.Json;
using System.Threading;
using Oxide.Core.Plugins;
using System.Collections;
using System.Diagnostics;
using Oxide.Game.Rust.Cui;
using Random = System.Random;
using System.Collections.Generic;
using Oxide.Core.Libraries.Covalence;
using System.Runtime.CompilerServices;
namespace Oxide.Plugins
{
[Info("Sync Pipes", "Joe 90", "0.9.27")]
[Description("Allows players to transfer items between containers. All pipes from a container are used synchronously to enable advanced sorting and splitting.")]
partial class SyncPipes : RustPlugin
{
#region Initialization
///
/// The instance of syncPipes on the server to allow child classes to access it
///
private static SyncPipes Instance;
#pragma warning disable CS0649
// Reference to the Furnace Splitter plugin https://umod.org/plugins/furnace-splitter
[PluginReference]
Plugin FurnaceSplitter;
// Refernce to the Quick Smelt plugin https://umod.org/plugins/quick-smelt
[PluginReference]
Plugin QuickSmelt;
#pragma warning restore CS0649
///
/// Hook: Initializes syncPipes when the server starts to load it
///
void Init()
{
Instance = this;
_config = SyncPipesConfig.Load();
Commands.InitializeChat();
permission.RegisterPermission($"{Name}.user", this);
permission.RegisterPermission($"{Name}.admin", this);
InstanceConfig.RegisterPermissions();
#region static data declarations
_chatCommands = new Dictionary {
{Oxide.Plugins.SyncPipes.Chat.Commands,true},
{Oxide.Plugins.SyncPipes.Overlay.CancelPipeCreationFromChat,true},
{Oxide.Plugins.SyncPipes.Overlay.CancelCopy,true},
{Oxide.Plugins.SyncPipes.Overlay.CancelRemove,true},
{Oxide.Plugins.SyncPipes.PipeMenu.HelpLabel.FlowBar,true},
};
_bindingCommands = new Dictionary {
{Oxide.Plugins.SyncPipes.Chat.PlacingBindingHint,true},
{Oxide.Plugins.SyncPipes.Overlay.CancelPipeCreationFromBind,true},
};
_messageTypes = new Dictionary {
{Oxide.Plugins.SyncPipes.Overlay.AlreadyConnected, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.TooFar, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.TooClose, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.NoPrivilegeToCreate, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.MonumentDenied, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.BlacklistedContainer, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.NoPrivilegeToEdit, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.PipeLimitReached, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.UpgradeLimitReached, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.HitFirstContainer, MessageType.Info},
{Oxide.Plugins.SyncPipes.Overlay.HitSecondContainer, MessageType.Info},
{Oxide.Plugins.SyncPipes.Overlay.HitToName, MessageType.Info},
{Oxide.Plugins.SyncPipes.Overlay.HitToClearName, MessageType.Info},
{Oxide.Plugins.SyncPipes.Overlay.CannotNameContainer, MessageType.Warning},
{Oxide.Plugins.SyncPipes.Overlay.CopyFromPipe, MessageType.Info},
{Oxide.Plugins.SyncPipes.Overlay.CopyToPipe, MessageType.Info},
{Oxide.Plugins.SyncPipes.Overlay.RemovePipe, MessageType.Info},
{Oxide.Plugins.SyncPipes.Pipe.Status.Pending, MessageType.Info},
{Oxide.Plugins.SyncPipes.Pipe.Status.Success, MessageType.Success},
{Oxide.Plugins.SyncPipes.Pipe.Status.SourceError, MessageType.Error},
{Oxide.Plugins.SyncPipes.Pipe.Status.DestinationError, MessageType.Error},
{Oxide.Plugins.SyncPipes.Pipe.Status.IdGenerationFailed, MessageType.Error},
};
_storageDetails = new Dictionary
{
{Storage.Default, new StorageData("unknown.container", "https://i.imgur.com/cayN7SQ.png", new Vector3(0f, 0f, 0f), false)},
{Storage.PumpJackCrudeOutput, new StorageData("crudeoutput", "c/c9/Pump_Jack_icon.png", new Vector3(0f, 0f, -0.5f), true)},
{Storage.Fireplace, new StorageData("fireplace.deployed", "https://static.wikia.nocookie.net/rust_gamepedia/images/c/c2/Stone_Fireplace.png/revision/latest/scale-to-width-down/{0}", new Vector3(0f, -1.3f, 0f), false)},
{Storage.ResearchTable, new StorageData("researchtable_deployed", "2/21/Research_Table_icon.png", new Vector3(0.8f, -0.5f, -0.3f), true)},
{Storage.VendingMachine, new StorageData("vendingmachine.deployed", "5/5c/Vending_Machine_icon.png", new Vector3(0f, -0.5f, 0f), true)},
{Storage.QuarryFuelInput, new StorageData("fuelstorage", "b/b8/Mining_Quarry_icon.png", new Vector3(-0.5f, 0f, -0.4f), true)},
{Storage.SmallPlanterBox, new StorageData("planter.small.deployed", "a/a7/Small_Planter_Box_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.JackOLanternHappy, new StorageData("jackolantern.happy", "9/92/Jack_O_Lantern_Happy_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.DropBox, new StorageData("dropbox.deployed", "4/46/Drop_Box_icon.png", new Vector3(0f, 0.4f, 0.3f), true)},
{Storage.MiningQuarry, new StorageData("mining_quarry", "b/b8/Mining_Quarry_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.SuperStocking, new StorageData("stocking_large_deployed", "6/6a/SUPER_Stocking_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.QuarryHopperOutput, new StorageData("hopperoutput", "b/b8/Mining_Quarry_icon.png", new Vector3(0f, -0.6f, -0.3f), true)},
{Storage.SmallOilRefinery, new StorageData("refinery_small_deployed", "a/ac/Small_Oil_Refinery_icon.png", new Vector3(-0.3f, -0.2f, -0.1f), true)},
{Storage.LargePlanterBox, new StorageData("planter.large.deployed", "3/35/Large_Planter_Box_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.ShotgunTrap, new StorageData("guntrap.deployed", "6/6c/Shotgun_Trap_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.LargeFurnace, new StorageData("furnace.large", "e/ee/Large_Furnace_icon.png", new Vector3(0f, -1.5f, 0f), true)},
{Storage.WoodStorageBox, new StorageData("woodbox_deployed", "f/ff/Wood_Storage_Box_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.PumpJack, new StorageData("mining.pumpjack", "c/c9/Pump_Jack_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.Recycler, new StorageData("recycler_static", "e/ef/Recycler_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.Fridge, new StorageData("fridge.deployed", "8/88/Fridge_icon.png", new Vector3(0f, -0.5f, 0f), true)},
{Storage.JackOLanternAngry, new StorageData("jackolantern.angry", "9/96/Jack_O_Lantern_Angry_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.SkullFirePit, new StorageData("skull_fire_pit", "3/32/Skull_Fire_Pit_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.Composter, new StorageData("composter", "https://i.imgur.com/qpA7I8P.png", new Vector3(0f, 0f, 0f), false)},
{Storage.CampFire, new StorageData("campfire", "3/35/Camp_Fire_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.LargeWoodBox, new StorageData("box.wooden.large", "b/b2/Large_Wood_Box_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.Barbeque, new StorageData("bbq.deployed", "f/f8/Barbeque_icon.png", new Vector3(-0.2f, -0.2f, 0f), true)},
{Storage.ToolCupboard, new StorageData("cupboard.tool.deployed", "5/57/Tool_Cupboard_icon.png", new Vector3(0f, -0.5f, 0f), true)},
{Storage.SmallStash, new StorageData("small_stash_deployed", "5/53/Small_Stash_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.Mailbox, new StorageData("mailbox.deployed", "1/17/Mail_Box_icon.png", new Vector3(0f, 0f, -0.15f), true)},
{Storage.Furnace, new StorageData("furnace", "e/e3/Furnace_icon.png", new Vector3(0f, -0.5f, 0f), true)},
{Storage.SurvivalFishTrap, new StorageData("survivalfishtrap.deployed", "9/9d/Survival_Fish_Trap_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.SmallStocking, new StorageData("stocking_small_deployed", "9/97/Small_Stocking_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.AutoTurret, new StorageData("autoturret_deployed", "f/f9/Auto_Turret_icon.png", new Vector3(0f, -0.58f, 0f), true)},
{Storage.RepairBench, new StorageData("repairbench_deployed", "3/3b/Repair_Bench_icon.png", new Vector3(0f, 0f, 0f), true)},
{Storage.FlameTurret, new StorageData("flameturret.deployed", "f/f9/Flame_Turret_icon.png", new Vector3(0f, -0.3f, 0.1f), true)},
{Storage.PumpJackFuelInput, new StorageData("fuelstorage", "c/c9/Pump_Jack_icon.png", new Vector3(-0.5f, 0.1f, -0.3f), true)},
};
#endregion
}
///
/// Hook: Cleans up syncPipes when the server unloads it
///
void Unload()
{
DataStore1_0.Save(false);
Puts("Unloading All Pipes");
Pipe.Cleanup();
ContainerManager.Cleanup();
PlayerHelper.Cleanup();
ExperimentalUnload();
}
partial void ExperimentalUnload();
#endregion
#region Commands
///
/// Contains all the commands used by syncPipes
/// Various stubs are included in the main SyncPipes class as this is needed by oxide
///
static class Commands
{
///
/// Adds all the chat commands to Oxide
///
public static void InitializeChat()
{
Add("", nameof(CommandArgs));
Add("help", nameof(CommandHelp));
Add("copy", nameof(CommandCopy));
Add("remove", nameof(CommandRemove));
Add("stats", nameof(CommandStats));
Add("name", nameof(CommandName));
}
///
/// Helper to simplify adding the commands to Oxide.
/// Adds the standard command prefix characters (from the config) to each command
///
/// The name of the command to be suffixed to the chat command charactes
/// The name of the method to be called by this command
private static void Add(string commandSuffix, string callback) =>
Instance.AddCovalenceCommand($"{InstanceConfig.CommandPrefix}{commandSuffix}", callback);
///
/// Default args based command
///
/// Player helper for the player calling this command
/// The arguments given with the command
public static void Args(PlayerHelper playerHelper, string[] args)
{
if (playerHelper == null) return;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return;
}
switch (args.Length > 0 ? args[0] : null)
{
case null:
case "p":
playerHelper.TogglePlacingPipe(false);
break;
case "h":
case "help":
case "?":
Help(playerHelper);
break;
case "c":
case "copy":
Copy(playerHelper);
break;
case "r":
case "remove":
Remove(playerHelper);
break;
case "s":
case "stats":
Stats(playerHelper);
break;
case "n":
var name = string.Join(" ", args.Length > 1 ? args[1] : null);
Name(playerHelper, name);
break;
}
}
///
/// Start or stop placing a pipe
///
/// The player calling the command
public static void PlacePipe(PlayerHelper playerHelper)
{
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return;
}
playerHelper?.TogglePlacingPipe(true);
}
///
/// Displays help information in the player chat bar.
///
/// Player requesting for help
public static void Help(PlayerHelper playerHelper)
{
if (playerHelper == null) return;
playerHelper.PrintToChatWithTitle(@"by Joe 90
Based on jPipes by TheGreatJ");
playerHelper.PrintToChat(Chat.Commands);
playerHelper.PrintToChat(Chat.PipeMenuInstructions);
playerHelper.PrintToChat(Chat.UpgradePipes);
}
///
/// Start or stop copying a pipe
///
/// Player wanting to copy a pipe
public static void Copy(PlayerHelper playerHelper) => playerHelper?.ToggleCopyingPipe();
///
/// Switch into or out of remove pipe mode
///
/// Player wanting to remove pipes
public static void Remove(PlayerHelper playerHelper)
{
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return;
}
playerHelper?.ToggleRemovingPipe();
}
///
/// Show player stats about how many pipes they have, their pipe limit (if applicable) and the state of those pipes.
///
/// Player requesting their stats
public static void Stats(PlayerHelper playerHelper)
{
if (playerHelper == null) return;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return;
}
var total = playerHelper.Pipes.Count;
var running = 0;
foreach (var pipe in playerHelper.Pipes)
{
if (pipe.Value.IsEnabled)
running++;
}
var disabled = total - running;
playerHelper.PrintToChatWithTitle(
playerHelper.PipeLimit != -1 ? Chat.StatsUnlimited : Chat.StatsLimited,
total,
playerHelper.PipeLimit,
running,
disabled
);
}
///
/// Open a pipe menu
///
/// Used to get the player and the pipe id
public static void OpenMenu(ConsoleSystem.Arg arg)
{
var playerHelper = PlayerHelper.Get(arg.Player());
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return;
}
GetPipe(arg)?.OpenMenu(playerHelper);
}
///
/// Close a pipe menu
///
/// Used to get the player and the pipe id
public static void CloseMenu(ConsoleSystem.Arg arg) => GetPipe(arg)?.CloseMenu(PlayerHelper.Get(arg.Player()));
///
/// Start or stop naming a pipe or container
///
/// Player wanting to name a pipe or container
/// The name to be applied
public static void Name(PlayerHelper playerHelper, string name)
{
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return;
}
playerHelper.StartNaming(name);
}
///
/// Close the player's menus. This is normally done when something affects the pipes they are looking at.
///
/// The player to close the menus for
public static void ForceCloseMenu(PlayerHelper playerHelper) => playerHelper?.CloseMenu();
///
/// Adjust the priority of the pipe
///
/// Used to get the player, the pipe id (arg.Args[0]) and the amount to change the priority by (arg.Args[1])
public static void ChangePriority(ConsoleSystem.Arg arg)
{
var pipe = GetPipe(arg);
var change = GetInt(arg);
if(change.HasValue)
pipe?.ChangePriority(change.Value);
}
///
/// Refresh a menu for a player. Normally called when a pipe is changed.
///
/// Used to get the player and the pipe id
public static void RefreshMenu(ConsoleSystem.Arg arg) => GetPipe(arg)?.OpenMenu(PlayerHelper.Get(arg.Player()));
///
/// Helper to get the pipe from a given command arg
///
/// Used the get the pipe id from arg.Args
/// Override the default index for the pipe id to be in the arg.Args
/// The pipe with the given pipe id
/// If it is missing or not pipe is found then it will return null
private static Pipe GetPipe(ConsoleSystem.Arg arg, int index = 0)
{
if (arg.Args.Length < index + 1) return null;
ulong pipeId;
return ulong.TryParse(arg.Args[index], out pipeId) ? Pipe.Get(pipeId) : null;
}
///
/// Helper to get a boolean value from a given command arg
///
/// Used to get the value to be parsed as bool
/// Set the index in the arg.Arg of the value to be parsed.
/// The boolean value of the input.
/// If it's missing or invalid it will return false
private static bool GetBool(ConsoleSystem.Arg arg, int index = 1)
{
if (arg.Args.Length < index + 1) return false;
return arg.Args[index]?.Equals(true.ToString()) ?? false;
}
///
/// Helper to get a nullable integer value from a give command arg
///
/// Used to get the value to be parsed as int
/// Set the index in the arg.Arg of the value to be parsed.
/// The integer value of the input
/// If it's missing or invalid it will return null
private static int? GetInt(ConsoleSystem.Arg arg, int index = 1)
{
if (arg.Args.Length < index + 1) return null;
int parseInt;
return int.TryParse(arg.Args[index], out parseInt) ? parseInt : default(int?);
}
///
/// Turn the pipe on or off
///
/// Used to get the pipe id and requested pipe state
public static void SetPipeState(ConsoleSystem.Arg arg) => GetPipe(arg)?.SetEnabled(GetBool(arg));
///
/// Turn the pipe's auto start on or off
///
/// Used to get the pipe id and requested auto start state
public static void SetPipeAutoStart(ConsoleSystem.Arg arg) => GetPipe(arg)?.SetAutoStart(GetBool(arg));
///
/// Reverse the direction of the pipe
///
/// Used to get the pipe id
public static void SwapPipeDirection(ConsoleSystem.Arg arg) => GetPipe(arg)?.SwapDirection();
///
/// Set the pipe to single or multi stack
///
/// Used to get the pipe id and the requested stack mode
/// true : set to multi-stack mode
/// false: set to single-stack mode
public static void SetPipeMultiStack(ConsoleSystem.Arg arg) => GetPipe(arg)?.SetMultiStack(GetBool(arg));
///
/// Turns on or off the pipe's Furnace Splitter options
///
/// Used to get the pipe id and the request Furnace Splitter state
public static void SetPipeFurnaceStackEnabled(ConsoleSystem.Arg arg) => GetPipe(arg)?.SetFurnaceStackEnabled(GetBool(arg));
///
/// Sets the number of stacks in the pipe's Furnace Stack Splitter options
///
/// Used to get the pipe id and the requested Furnace Splitter stack count
public static void SetPipeFurnaceStackCount(ConsoleSystem.Arg arg)
{
var stackCount = GetInt(arg);
if (stackCount == null) return;
GetPipe(arg)?.SetFurnaceStackCount(stackCount.Value);
}
///
/// Opens a loot container that allows players to control the items a pipe filters by
///
/// Used to get the pipe id and the player
public static void OpenPipeFilter(ConsoleSystem.Arg arg) => GetPipe(arg)?.OpenFilter(PlayerHelper.Get(arg.Player()));
///
/// Shows or hides help labels in pipe menu
///
/// Used to get the pipe id and the player
public static void MenuHelp(ConsoleSystem.Arg arg) => PlayerHelper.Get(arg.Player())?.Menu.ToggleHelp();
// Flush the permissions of this player helper by forcing it to be recreated
public static void FlushPlayerPermissions(ConsoleSystem.Arg arg) => PlayerHelper.Remove(arg.Player());
}
#region Command Stubs
// These stubs are included as Oxide.Plugin needs all command and chat functions in the main class.
void CommandArgs(IPlayer player, string command, string[] args) => Commands.Args(PlayerHelper.Get(player), args);
void CommandHelp(IPlayer player, string command, string[] args) => Commands.Help(PlayerHelper.Get(player));
void CommandCopy(IPlayer player, string command, string[] args) => Commands.Copy(PlayerHelper.Get(player));
void CommandRemove(IPlayer player, string command, string[] args) => Commands.Remove(PlayerHelper.Get(player));
void CommandStats(IPlayer player, string command, string[] args) => Commands.Stats(PlayerHelper.Get(player));
void CommandName(IPlayer player, string command, string[] args) => Commands.Name(PlayerHelper.Get(player), string.Join(" ", args));
[SyncPipesConsoleCommand("create")]
void StartPipe(ConsoleSystem.Arg arg) => Commands.PlacePipe(PlayerHelper.Get(arg.Player()));
[SyncPipesConsoleCommand("openmenu")]
void OpenMenu(ConsoleSystem.Arg arg) => Commands.OpenMenu(arg);
[SyncPipesConsoleCommand("closemenu")]
void CloseMenu(ConsoleSystem.Arg arg) => Commands.CloseMenu(arg);
[SyncPipesConsoleCommand("forceclosemenu")]
void ForceCloseMenu(ConsoleSystem.Arg arg) => Commands.ForceCloseMenu(PlayerHelper.Get(arg.Player()));
[SyncPipesConsoleCommand("refreshmenu")]
void RefreshMenu(ConsoleSystem.Arg arg) => Commands.RefreshMenu(arg);
[SyncPipesConsoleCommand("changepriority")]
void ChangePriority(ConsoleSystem.Arg arg) => Commands.ChangePriority(arg);
[SyncPipesConsoleCommand("setpipestate")]
void SetPipeState(ConsoleSystem.Arg arg) => Commands.SetPipeState(arg);
[SyncPipesConsoleCommand("setpipeautostart")]
void SetPipeAutoStart(ConsoleSystem.Arg arg) => Commands.SetPipeAutoStart(arg);
[SyncPipesConsoleCommand("swappipedirection")]
void SwapPipeDirection(ConsoleSystem.Arg arg) => Commands.SwapPipeDirection(arg);
[SyncPipesConsoleCommand("setpipemultistack")]
void SetPipeMultiStack(ConsoleSystem.Arg arg) => Commands.SetPipeMultiStack(arg);
[SyncPipesConsoleCommand("setpipefurnacestackenabled")]
void SetPipeFurnaceStackEnabled(ConsoleSystem.Arg arg) => Commands.SetPipeFurnaceStackEnabled(arg);
[SyncPipesConsoleCommand("setpipefurnacestackcount")]
void SetPipeFurnaceStackCount(ConsoleSystem.Arg arg) => Commands.SetPipeFurnaceStackCount(arg);
[SyncPipesConsoleCommand("openpipefilter")]
void OpenPipeFilter(ConsoleSystem.Arg arg) => Commands.OpenPipeFilter(arg);
[SyncPipesConsoleCommand("menuhelp")]
void MenuHelp(ConsoleSystem.Arg arg) => Commands.MenuHelp(arg);
[SyncPipesConsoleCommand("flushperms")]
void FlushPlayerPermissions(ConsoleSystem.Arg arg) => Commands.FlushPlayerPermissions(arg);
#endregion
///
/// Helper to add the plugin commandPrefix to the start of each command.
///
public class SyncPipesConsoleCommandAttribute : ConsoleCommandAttribute
{
public SyncPipesConsoleCommandAttribute(string command) : base($"{nameof(SyncPipes).ToLower()}.{command}") { }
}
#endregion
#region Config
class SyncPipesConfig
{
private static readonly SyncPipesConfig Default = New();
public static SyncPipesConfig New()
{
return new SyncPipesConfig
{
FilterSizes = new List { 0, 6, 18, 30, 42 },
FlowRates = new List { 1, 5, 10, 30, 50 },
MaximumPipeDistance = 64f,
MinimumPipeDistance = 2f,
NoDecay = true,
CommandPrefix = "p",
HotKey = "p",
UpdateRate = 2,
AttachXmasLights = false,
DestroyWithSalvage = false,
PermissionLevels = new Dictionary
{
{"sticks", new PermissionLevel{MaximumGrade = 0, MaximumPipes = 15}},
{"wood", new PermissionLevel{MaximumGrade = 1, MaximumPipes = 25}},
{"stone", new PermissionLevel{MaximumGrade = 2, MaximumPipes = 35}},
{"metal", new PermissionLevel{MaximumGrade = 3, MaximumPipes = 45}},
{"hqm", new PermissionLevel{MaximumGrade = -1, MaximumPipes = -1}}
}
};
}
[JsonProperty("LogLevel")]
public int LogLevel { get; set; } = (int)LogLevels.Error;
[JsonProperty("filterSizes")]
public List FilterSizes { get; set; }
[JsonProperty("flowRates")]
public List FlowRates { get; set; }
[JsonProperty("maxPipeDist")]
public float MaximumPipeDistance { get; set; }
[JsonProperty("minPipeDist")]
public float MinimumPipeDistance { get; set; }
[JsonProperty("noDecay")]
public bool NoDecay { get; set; }
[JsonProperty("commandPrefix")]
public string CommandPrefix { get; set; }
[JsonProperty("hotKey")]
public string HotKey { get; set; }
[JsonProperty("updateRate")]
public int UpdateRate { get; set; }
[JsonProperty("xmasLights")]
public bool AttachXmasLights { get; set; }
[JsonProperty("permLevels", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary PermissionLevels { get; set; }
[JsonProperty("salvageDestroy")] public bool DestroyWithSalvage { get; set; } = false;
[JsonProperty("experimental", DefaultValueHandling = DefaultValueHandling.Ignore)]
public ExperimentalConfig Experimental { get; set; }
public class PermissionLevel
{
[JsonProperty("upgradeLimit")]
public int MaximumGrade { get; set; } = (int)BuildingGrade.Enum.TopTier;
[JsonProperty("pipeLimit")]
public int MaximumPipes { get; set; } = -1;
public static readonly PermissionLevel Default = new PermissionLevel() {MaximumGrade = (int)BuildingGrade.Enum.Twigs, MaximumPipes = 0};
}
private string[] Validate()
{
var errors = new List();
var filterSizeError = FilterSizes.Count != 5;
if (!filterSizeError)
{
for (var i = 0; i < FilterSizes.Count; i++)
{
if (FilterSizes[i] < 0 || FilterSizes[i] > 42)
{
filterSizeError = true;
break;
}
}
}
if (filterSizeError)
{
errors.Add("filterSizes must have 5 values between 0 and 42");
FilterSizes = new List(Default.FilterSizes);
}
var flowRateError = FlowRates.Count != 5;
if (!flowRateError)
{
for (var i = 0; i < FlowRates.Count; i++)
{
if (FlowRates[i] <= 0)
{
flowRateError = true;
break;
}
}
}
if (flowRateError)
{
errors.Add("flowRates must have 5 values greater than 0");
FlowRates = new List(Default.FlowRates);
}
if (UpdateRate <= 0)
{
errors.Add("updateRage must be greater than 0");
UpdateRate = Default.UpdateRate;
}
return errors.ToArray();
}
public static SyncPipesConfig Load()
{
try
{
Instance.Puts("Loading Config");
var config = Instance.Config.ReadObject();
if (config?.FilterSizes == null)
{
Instance.Puts("Setting Defaults");
config = New();
Instance.Config.WriteObject(config);
}
var errors = config.Validate();
for (var i = 0; i < errors.Length; i++)
Instance.PrintWarning(errors[i]);
if (errors.Length > 0)
{
Instance.PrintError("Invalid config file. Using default configs.");
return Default;
}
return config;
}
catch (Exception e)
{
Logger.Runtime.LogException(e, "Config.Load");
Instance.PrintError("Invalid config file. Using default configs.");
return Default;
}
}
///
/// Register the level permission keys to Oxide
///
public void RegisterPermissions()
{
if (PermissionLevels != null)
{
foreach (var permissionKey in PermissionLevels.Keys)
{
Instance.permission.RegisterPermission($"{Instance.Name}.level.{permissionKey}", Instance);
}
}
}
}
class ExperimentalConfig
{
[JsonProperty("barrelPipe")]
public bool BarrelPipe { get; set; }
}
///
/// Oxide hook for loading default config settings
///
protected override void LoadDefaultConfig()
{
Config?.Clear();
_config = SyncPipesConfig.New();
Config?.WriteObject(_config);
SaveConfig();
}
///
/// Config for this plugin instance.
///
static SyncPipesConfig InstanceConfig => Instance._config;
private SyncPipesConfig _config; // the config store for this plugin instance
///
/// New Hook: Exposes the No Decay config to external plugins
///
private bool IsNoDecayEnabled => InstanceConfig.NoDecay;
#endregion
#region ContainerHelper
public const string FUEL_STORAGE_PREFAB = "fuelstorage";
public const string QUARRY_OUTPUT_PREFAB = "hopperoutput";
public const string PUMPJACK_OUTPUT_PREFAB = "crudeoutput";
///
/// This helps find containers and the required information needed to attach pipes
///
static class ContainerHelper
{
///
/// Lists all the container types that pipes cannot connect to
///
/// The container to check
/// True if the container type is blacklisted
public static bool IsBlacklisted(BaseEntity container) =>
container is BaseFuelLightSource || container is Locker || container is ShopFront ||
container is RepairBench || container is LootContainer;
///
/// Get a storage container from its Id
///
/// The Id to search for
/// The container that matches the id
public static StorageContainer Find(uint id) => Find((BaseEntity) BaseNetworkable.serverEntities.Find(id));
///
/// Get the container id and the startable type from a container
///
/// The container to get the data for
public static ContainerType GetEntityType(BaseEntity container)
{
if (container is BaseOven)
return ContainerType.Oven;
if (container is Recycler)
return ContainerType.Recycler;
if (container is ResourceExtractorFuelStorage)
{
switch (container.ShortPrefabName)
{
case FUEL_STORAGE_PREFAB:
return ContainerType.FuelStorage;
case QUARRY_OUTPUT_PREFAB:
return ContainerType.QuarryOutput;
case PUMPJACK_OUTPUT_PREFAB:
return ContainerType.PumpJackOutput;
}
}
return ContainerType.General;
}
public static bool InMonument(BaseEntity entity)
{
switch (GetEntityType(entity))
{
case ContainerType.PumpJackOutput:
case ContainerType.QuarryOutput:
case ContainerType.FuelStorage:
case ContainerType.Recycler:
for (int i = 0; i < TerrainMeta.Path.Monuments.Count; i++)
{
var monument = TerrainMeta.Path.Monuments[i];
if (monument.IsInBounds(entity.transform.position))
return false;
}
break;
}
return true;
}
private static void LogFindError(uint parentId, BaseEntity entity, ContainerType containerType, List children = null)
{
Logger.FindErrors.Log("------------------- {0} -------------------", parentId);
if (entity == null)
Logger.FindErrors.Log("Entity not found");
else
Logger.FindErrors.Log("Entity: {0} ({1})", entity.ShortPrefabName, entity);
Logger.FindErrors.Log("Type: {0}", containerType);
for (int i = 0; i < children?.Count; i++)
Logger.FindErrors.Log("Child {0}: {1} ({2})", i, children[i].ShortPrefabName, children[i]);
Logger.FindErrors.Log("");
}
public static BaseEntity Find(uint parentId, ContainerType containerType)
{
var entity = (BaseEntity) BaseNetworkable.serverEntities.Find(parentId);
if (entity == null)
{
LogFindError(parentId, null, containerType);
return null;
}
if (!IsComplexStorage(containerType))
return entity;
var children = entity?.GetComponent()?.children;
var prefabName = GetShortPrefabName(containerType);
for (var i = 0; i < children?.Count; i++)
{
if (children[i].ShortPrefabName == prefabName)
return children[i] as ResourceExtractorFuelStorage;
}
LogFindError(parentId, entity, containerType, children);
return null;
}
public static StorageContainer Find(BaseEntity parent) => parent?.GetComponent();
public static string GetShortPrefabName(ContainerType containerType)
{
switch (containerType)
{
case ContainerType.FuelStorage:
return FUEL_STORAGE_PREFAB;
case ContainerType.QuarryOutput:
return QUARRY_OUTPUT_PREFAB;
case ContainerType.PumpJackOutput:
return PUMPJACK_OUTPUT_PREFAB;
}
return "";
}
public static bool IsComplexStorage(ContainerType containerType)
{
switch (containerType)
{
case ContainerType.FuelStorage:
case ContainerType.PumpJackOutput:
case ContainerType.QuarryOutput:
return true;
default:
return false;
}
}
public static bool CanAutoStart(ContainerType containerType)
{
switch (containerType)
{
case ContainerType.FuelStorage:
case ContainerType.Oven:
case ContainerType.Recycler:
return true;
default:
return false;
}
}
}
///
/// Entity Types
///
public enum ContainerType
{
General,
Oven,
Recycler,
FuelStorage,
QuarryOutput,
PumpJackOutput,
ResourceExtractor
}
#endregion
#region ContainerManager
///
/// This is attached to a Storage Container to act as the controller for moving items through pipes.
/// This then allows for items to move through pipes in a more synchronous manner.
/// Items can be split evenly between all pipes of the same priority.
///
[JsonConverter(typeof(ContainerManager.Converter))]
public class ContainerManager : MonoBehaviour
{
///
/// This is the serializable data format fro loading or saving container manager data
///
public class Data
{
public uint ContainerId;
public bool CombineStacks;
public string DisplayName;
public ContainerType ContainerType;
///
/// This is required to deserialize from json
///
public Data() { }
///
/// Create data from a container manager for saving
///
/// Container manager to extract settings from
public Data(ContainerManager containerManager)
{
ContainerId = containerManager.ContainerId;
CombineStacks = containerManager.CombineStacks;
DisplayName = containerManager.DisplayName;
ContainerType = ContainerHelper.GetEntityType(containerManager._container);
}
}
///
/// Get the save data for all container managers
///
/// data for all container managers
public static IEnumerable Save()
{
using (var enumerator = ManagedContainerLookup.GetEnumerator())
{
while (enumerator.MoveNext())
{
if (enumerator.Current.Value.HasAnyPipes)
yield return new Data(enumerator.Current.Value);
}
}
}
private static void LogLoadError(Data data)
{
Logger.ContainerLoader.Log("------------------- {0} -------------------", data.ContainerId);
Logger.ContainerLoader.Log("Container Type: {0}", data.ContainerType);
Logger.ContainerLoader.Log("Display Name: {0}", data.DisplayName);
Logger.ContainerLoader.Log("");
}
///
/// Load all data into the container managers.
/// This must be run after Pipe.Load as it only updates container managers created by the pipes.
///
/// Data to load into container managers
public static void Load(List dataToLoad)
{
if (dataToLoad == null) return;
var containerCount = 0;
for(int i = 0; i < dataToLoad.Count; i++)
{
ContainerManager manager;
if (ContainerHelper.IsComplexStorage(dataToLoad[i].ContainerType))
{
var entity = ContainerHelper.Find(dataToLoad[i].ContainerId, dataToLoad[i].ContainerType);
dataToLoad[i].ContainerId = entity?.net.ID ?? 0;
}
if (ManagedContainerLookup.TryGetValue(dataToLoad[i].ContainerId, out manager))
{
containerCount++;
manager.DisplayName = dataToLoad[i].DisplayName;
manager.CombineStacks = dataToLoad[i].CombineStacks;
}
else
{
Instance.PrintWarning("Failed to load manager [{0} - {1} - {2}]: Container not found", dataToLoad[i].ContainerId, dataToLoad[i].ContainerType, dataToLoad[i].DisplayName);
LogLoadError(dataToLoad[i]);
}
}
Instance.Puts("Successfully loaded {0} managers", containerCount);
}
///
/// Keeps track of all the container managers that have been created.
///
private static readonly Dictionary ManagedContainerLookup =
new Dictionary();
public static readonly List ManagedContainers = new List();
// Which pipes have been attached to this container manager
//private readonly Dictionary _attachedPipeLookup = new Dictionary();
private readonly List _attachedPipes = new List();
// Pull from multiple stack of the same type whe moving or only move one stack per priority level
// This has been implemented but the controlling systems have not been developed
public bool CombineStacks { get; private set; } = true;
private StorageContainer _container; // The storage container this manager is attached to
public uint ContainerId; // The id of the storage container this manager is attached to
private float _cumulativeDeltaTime; // Used to keep track of the time between each cycle
private bool _destroyed; // Prevents move cycles from happening when the container is being destroyed
public string DisplayName; // The name of this container
///
/// Checks if there are any pipes attached to this container.
///
public bool HasAnyPipes => _attachedPipes.Count > 0;
///
/// Cleanup all container managers. Normally used at unload.
///
public static void Cleanup()
{
while (ManagedContainers.Count > 0)
{
if(ManagedContainers[0] == null)
ManagedContainers.RemoveAt(0);
else
ManagedContainers[0].Kill(true);
}
ManagedContainerLookup.Clear();
ManagedContainers.Clear();
}
///
/// Destroy this Container manager and any attached pipes
///
///
/// Is this a cleanup call.
/// If this is false then the pipes will animate when they are destroyed.
///
private void Kill(bool cleanup = false)
{
for(var i = 0; i < _attachedPipes.Count; i++)
{
if (_attachedPipes[i]?.Destination?.ContainerManager == this)
_attachedPipes[i].Remove(cleanup);
if (_attachedPipes[i]?.Source?.ContainerManager == this)
_attachedPipes[i].Remove(cleanup);
}
_destroyed = true;
if (ManagedContainerLookup.ContainsKey(ContainerId))
{
ManagedContainerLookup.Remove(ContainerId);
ManagedContainers.Remove(this);
}
Destroy(this);
}
///
/// Locate exist container manager for this container or create a new one then attach it to the container.
///
/// Entity to attach the manager to
/// Container for this entity
/// Pipe to attach
///
public static ContainerManager Attach(BaseEntity entity, StorageContainer container, Pipe pipe)
{
if (entity == null || container == null || pipe == null) return null;
ContainerManager containerManager = null;
if (!ManagedContainerLookup.ContainsKey(entity.net.ID))
{
containerManager = entity.gameObject.AddComponent();
ManagedContainerLookup.Add(entity.net.ID, containerManager);
ManagedContainers.Add(containerManager);
}
else
{
containerManager = ManagedContainerLookup[entity.net.ID];
}
if (!containerManager._attachedPipes.Contains(pipe))
{
containerManager._attachedPipes.Add(pipe);
}
containerManager.ContainerId = entity.net.ID;
containerManager._container = container;
return containerManager;
}
///
/// Detach a pipe from the container manager
///
/// Id of the container to identify the container manager
/// The pipe to detach
public static void Detach(uint containerId, Pipe pipe)
{
try
{
if (pipe != null && ManagedContainerLookup.ContainsKey(containerId))
{
var containerManager = ManagedContainerLookup[containerId];
if (containerManager._attachedPipes?.Contains(pipe) ?? false)
containerManager._attachedPipes?.Remove(pipe);
}
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(Detach));
}
}
///
/// Hook: Check container and if still valid and cycle time has elapsed then move items along pipes
///
private void Update()
{
try
{
if (_container == null)
Kill();
if (_destroyed || !HasAnyPipes) return;
_cumulativeDeltaTime += Time.deltaTime;
if (_cumulativeDeltaTime < InstanceConfig.UpdateRate) return;
_cumulativeDeltaTime = 0f;
if (_container.inventory.itemList.Count == 0 || _container.inventory.itemList[0] == null)
return;
var pipeGroups = new Dictionary>>();
for (var i = 0; i < _attachedPipes.Count; i++)
{
var pipe = _attachedPipes[i];
if (_attachedPipes[i].Source.Container != _container || !_attachedPipes[i].IsEnabled)
continue;
var priority = (int) pipe.Priority;
var grade = (int) pipe.Grade;
if (!pipeGroups.ContainsKey(priority))
pipeGroups.Add(priority, new Dictionary>());
if (!pipeGroups[priority].ContainsKey(grade))
pipeGroups[priority].Add(grade, new List());
pipeGroups[priority][grade].Add(pipe);
}
//var pipeGroups = _attachedPipeLookup.Values.Where(a => a.Source.ContainerManager == this)
// .GroupBy(a => a.Priority).OrderByDescending(a => a.Key).ToArray();
for (int i = (int) Pipe.PipePriority.Highest; i > (int) Pipe.PipePriority.Demand; i--)
{
if (!pipeGroups.ContainsKey(i)) continue;
if (CombineStacks)
MoveCombineStacks(pipeGroups[i]);
else
MoveIndividualStacks(pipeGroups[i]);
}
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(Update));
}
}
private List ItemList
{
get
{
if (_container is Recycler)
{
var itemList = new List();
for (int i = 6; i < 12; i++)
{
var item = _container.inventory.GetSlot(i);
if (item == null) continue;
itemList.Add(item);
}
return itemList;
}
return _container.inventory.itemList;
}
}
private MovableType CanPuItem(Item item)
{
try
{
var oven = _container as BaseOven;
if (oven == null) return MovableType.Allowed;
if (
item.info.category != ItemCategory.Resources &&
item.info.category != ItemCategory.Food ||
item.info.shortname.EndsWith("cooked") ||
item.info.shortname.EndsWith("burned")
) return MovableType.Rejected;
var fuel = item.info.GetComponent();
if (fuel != null)
return oven.fuelType?.Equals(item.info) ?? false ? MovableType.Fuel : MovableType.Rejected;
var cookable = item.info.GetComponent();
if (cookable != null &&
cookable.lowTemp <= oven.cookingTemperature &&
cookable.highTemp >= oven.cookingTemperature)
return MovableType.Cookable;
return MovableType.Rejected;
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(CanPuItem));
return MovableType.Rejected;
}
}
private bool CanTakeItem(Item item)
{
try
{
var oven = _container as BaseOven;
if (oven == null) return true;
if (
item.info.category != ItemCategory.Resources &&
item.info.category != ItemCategory.Food ||
item.info.shortname.ToLower().EndsWith("cooked") ||
item.info.shortname.EndsWith("burned")
) return true;
var fuel = item.info.GetComponent();
if (fuel != null)
return !oven.fuelType.Equals(item.info);
var cookable = item.info.GetComponent();
if (cookable != null &&
cookable.lowTemp <= oven.cookingTemperature &&
cookable.highTemp >= oven.cookingTemperature)
return false;
return true;
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(CanTakeItem));
return false;
}
}
private enum MovableType
{
Allowed,
Cookable,
Fuel,
Rejected
}
///
/// Attempt to move all items from all stacks of the same type down the pipes in this priroity group
/// Items will be split as evenly as possible down all the pipes (limited by flow rate)
///
/// Pipes grouped by their priority
private void MoveCombineStacks(Dictionary> pipeGroup)
{
try
{
var distinctItemIds = new List();
var distinctItems = new Dictionary>();
var itemList = ItemList;
for (var i = 0; i < itemList.Count; i++)
{
var itemId = itemList[i].info.itemid;
if (!distinctItems.ContainsKey(itemList[i].info.itemid))
{
distinctItems.Add(itemId, new List());
distinctItemIds.Add(itemId);
}
distinctItems[itemId].Add(itemList[i]);
}
var unusedPipes = new List();
for (var i = (int) BuildingGrade.Enum.Twigs; i <= (int) BuildingGrade.Enum.TopTier; i++)
{
if (!pipeGroup.ContainsKey(i))
continue;
for (var j = 0; j < pipeGroup[i].Count; j++)
{
var pipe = pipeGroup[i][j];
if (pipe.Source.Id != ContainerId)
continue;
if (pipe.PipeFilter.Items.Count > 0)
{
var found = false;
for (var k = 0; k < pipe.PipeFilter.Items.Count; k++)
{
if (distinctItems.ContainsKey(pipe.PipeFilter.Items[k].info.itemid))
found = true;
}
if (!found)
continue;
}
unusedPipes.Add(pipe);
}
}
while (unusedPipes.Count > 0 && distinctItems.Count > 0)
{
var itemId = distinctItemIds[0];
var item = distinctItems[itemId];
distinctItems.Remove(distinctItemIds[0]);
distinctItemIds.RemoveAt(0);
var quantity = 0;
for (var i = 0; i < item.Count; i++)
quantity += item[0].amount;
var validPipes = new List();
for (var i = 0; i < unusedPipes.Count; i++)
{
var pipe = unusedPipes[i];
if (pipe.Destination.Storage.inventory.CanAcceptItem(item[0], 0) == ItemContainer.CanAcceptResult.CannotAccept)
continue;
if (pipe.PipeFilter.Items.Count > 0)
{
bool found = false;
for (var j = 0; j < pipe.PipeFilter.Items.Count; j++)
{
if (pipe.PipeFilter.Items[j].info.itemid == itemId)
found = true;
}
if (!found)
continue;
}
validPipes.Add(pipe);
}
var pipesLeft = validPipes.Count;
for (var i = 0; i < validPipes.Count; i++)
{
var validPipe = validPipes[i];
var recycler = validPipe.Destination.Storage as Recycler;
if (recycler != null && !recycler.RecyclerItemFilter(item[0], -1))
continue;
var canPut = validPipe.Destination.ContainerManager.CanPuItem(item[0]);
var canTake = CanTakeItem(item[0]);
if (canPut == MovableType.Rejected || !canTake)
continue;
var amountToMove = GetAmountToMove(itemId, quantity, pipesLeft--, validPipe,
item[0]?.MaxStackable() ?? 0, validPipe.IsMultiStack && canPut != MovableType.Fuel);
if (amountToMove <= 0)
break;
quantity -= amountToMove;
for (var j = 0; j < item.Count; j++)
{
var itemStack = item[j];
var toMove = itemStack;
if (amountToMove <= 0) break;
if (amountToMove < itemStack.amount)
toMove = itemStack.SplitItem(amountToMove);
unusedPipes.Remove(validPipe);
if (Instance.FurnaceSplitter != null &&
canPut != MovableType.Fuel &&
validPipe.Destination.ContainerType == ContainerType.Oven &&
validPipe.IsFurnaceSplitterEnabled && validPipe.FurnaceSplitterStacks > 1)
{
var result = Instance.FurnaceSplitter.Call("MoveSplitItem", toMove,
validPipe.Destination.Storage,
validPipe.FurnaceSplitterStacks);
if (!result.ToString().Equals("ok", StringComparison.InvariantCultureIgnoreCase))
toMove.MoveToContainer(validPipe.Source.Storage.inventory);
}
else
{
var toContainer = validPipe.Destination.Storage.inventory;
if (!toMove.MoveToContainer(toContainer))
{
// Fix for issue with Vending machines not being able to move the end of a stack stack from a container.
// Remove item from container and then move it. If it didn't actually move then add it back to the source.
toMove.RemoveFromContainer();
if (!toMove.MoveToContainer(toContainer))
toMove.MoveToContainer(validPipe.Source.Storage.inventory);
}
}
if (validPipe.IsAutoStart && validPipe.Destination.HasFuel())
validPipe.Destination.Start();
amountToMove -= toMove.amount;
}
// If all items have been taken allow the pipe to transport something else. This will only occur if the initial quantity is less than the number of pipes
if (quantity <= 0)
break;
}
}
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(MoveCombineStacks));
}
}
///
/// Attempt to move items from the first stack down the pipes in this priority group
/// Items will be split as evenly as possible down all the pipes (limited by flow rate)
///
/// Pipes grouped by their priority
private void MoveIndividualStacks(Dictionary> pipeGroup)
{
for (var i = 0; i < (int) BuildingGrade.Enum.TopTier; i++)
{
if (!pipeGroup.ContainsKey(i))
continue;
var pipes = pipeGroup[i];
for (var j = 0; j < pipes.Count; j++)
{
var pipe = pipes[j];
var item = _container.inventory.itemList.Count > 0 ? _container.inventory.itemList[0] : null;
if (item == null) return;
GetItemToMove(item, pipe)?.MoveToContainer(pipe.Destination.Storage.inventory);
}
}
}
///
/// Determines the maximum quantity of the item can be moved down a pipe in this cycle
///
/// The id of the item to be moved
/// The total number of items available to move
/// How many more pipes in this pipe group are left
/// The pipe to move items along
/// The maximum stack size of this item. Used to check available space
///
private int GetAmountToMove(int itemId, int itemQuantity, int pipesLeft, Pipe pipe, int maxStackable, bool multiStack)
{
try
{
var destinationContainer = pipe?.Destination.Storage;
if (destinationContainer == null || maxStackable == 0) return 0;
var amountToMove = (int) Math.Ceiling((decimal) itemQuantity / pipesLeft);
if (amountToMove > pipe.FlowRate)
amountToMove = pipe.FlowRate;
var emptySlots = destinationContainer.inventory.capacity -
destinationContainer.inventory.itemList.Count;
var itemStacks = destinationContainer.inventory.FindItemsByItemID(itemId);
int minStackSize = GetMinStackSize(itemStacks);
if (minStackSize <= 0 && emptySlots == 0)
return 0;
if (!multiStack)
{
var stackCapacity = maxStackable - minStackSize;
if (minStackSize > 0)
return amountToMove <= stackCapacity ? amountToMove : stackCapacity;
return amountToMove;
}
var slotsRequired = (int) Math.Ceiling((decimal) amountToMove / maxStackable);
if (slotsRequired <= emptySlots)
return amountToMove;
var neededSpace = amountToMove % maxStackable;
return maxStackable - minStackSize >= neededSpace
? amountToMove
: maxStackable * (slotsRequired - 1) + maxStackable - minStackSize;
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(GetAmountToMove));
return 0;
}
}
///
/// Prepare the item to be moved along the pipe
/// This takes into account available space in the destination and flow rate
///
/// The item to be moved
/// The pipe to move the item along
///
private Item GetItemToMove(Item item, Pipe pipe)
{
try
{
var destinationContainer = pipe.Destination.Storage;
if (destinationContainer == null) return null;
var maxStackable = item.MaxStackable();
if (item.amount > pipe.FlowRate)
item.SplitItem(pipe.FlowRate);
var noEmptyStacks = destinationContainer.inventory.itemList.Count ==
destinationContainer.inventory.capacity;
if (!pipe.IsMultiStack || noEmptyStacks)
{
var itemStacks = destinationContainer.inventory.FindItemsByItemID(item.info.itemid);
int minStackSize = GetMinStackSize(itemStacks);
if (minStackSize == 0 && noEmptyStacks || minStackSize == maxStackable)
return null;
var space = maxStackable - minStackSize;
if (space < item.amount)
return item.SplitItem(space);
return item;
}
return item;
}
catch (Exception e)
{
Logger.Runtime.LogException(e, nameof(GetItemToMove));
return item;
}
}
private static int GetMinStackSize(List itemStacks)
{
int minStackSize = -1;
for (var i = 0; i < itemStacks.Count; i++)
{
if (minStackSize < 0 || itemStacks[i].amount < minStackSize)
minStackSize = itemStacks[i].amount;
}
return minStackSize < 0 ? 0 : minStackSize;
}
public class Converter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var container = value as ContainerManager;
if (container == null) return;
writer.WriteStartObject();
writer.WritePropertyName("ci");
if(container._container is ResourceExtractorFuelStorage)
writer.WriteValue(container._container.parentEntity.uid);
else
writer.WriteValue(container.ContainerId);
writer.WritePropertyName("cs");
writer.WriteValue(container.CombineStacks);
writer.WritePropertyName("dn");
writer.WriteValue(container.DisplayName);
writer.WritePropertyName("ct");
writer.WriteValue(ContainerHelper.GetEntityType(container._container));
writer.WriteEndObject();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ContainerManager);
}
}
}
#endregion
#region CuiBase
internal abstract class CuiMenuBase: CuiBase, IDisposable
{
protected PlayerHelper PlayerHelper { get; }
protected CuiMenuBase(PlayerHelper playerHelper)
{
PlayerHelper = playerHelper;
}
protected CuiElementContainer Container { get; private set; } = new CuiElementContainer() { };
protected string PrimaryPanel { get; set; }
protected bool Visible { get; private set; }
public virtual void Show()
{
if(PrimaryPanel != null)
CuiHelper.AddUi(PlayerHelper.Player, Container);
Visible = true;
}
public virtual void Close()
{
if (PrimaryPanel != null)
CuiHelper.DestroyUi(PlayerHelper.Player, PrimaryPanel);
Visible = false;
}
public virtual void Refresh()
{
Close();
Show();
}
///
/// Helper to make commands for button actions. It will automatically prefix the command prefix and append to pipe id.
///
/// The command name for the action to be performed (what appears after the dot)
/// All required for the command (the pipe id is added automatically as the first arg)
/// Fully formed string for the command to be run
protected string MakeCommand(string commandName, params object[] args)
{
var command = $"{Instance.Name.ToLower()}.{commandName} {string.Join(" ", args)}";
return command;
}
protected string AddPanel(string parent, string min, string max, string colour = "0 0 0 0",
bool cursorEnabled = false)
{
CuiPanel panel;
return AddPanel(parent, min, max, colour, cursorEnabled, out panel);
}
///
/// Add a CUI Panel to the main elements container
///
/// Cui parent Id
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// "R G B A" colour of the panel
/// Enable Cursor interaction with this panel
/// Panel Id. Used as parent input for other CUI elements
protected string AddPanel(string parent, string min, string max, string colour,
bool cursorEnabled, out CuiPanel panel)
{
panel = MakePanel(min, max, colour, cursorEnabled);
return Container.Add(panel, parent);
}
///
/// Add a CUI Label to the main elements container
///
/// CUI parent Id
/// Text to display
/// Text font size
/// Text Alignment
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// "R G B A" colour of the panel
protected void AddLabel(string parent, string text, int fontSize, TextAnchor alignment, string min = "0 0", string max = "1 1", string colour = "1 1 1 1") =>
Container.Add(MakeLabel(text, fontSize, alignment, min, max, colour), parent);
protected void AddLabelWithOutline(string parent, string text, int fontSize, TextAnchor alignment,
string min = "0 0", string max = "1 1", string textColour = "1 1 1 1",
string outlineColour = "0.15 0.15 0.15 0.43", string distance = "1.1 -1.1",
bool useGraphicAlpha = false)
{
var labelWithOutline = MakeLabelWithOutline(text, fontSize, alignment, min, max, textColour,
outlineColour, distance, useGraphicAlpha);
labelWithOutline.Parent = parent;
Container.Add(labelWithOutline);
}
///
/// Add the CUI Element with an image to the main elements container
///
/// CUI parent Id
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// Url of the image to show
/// "R G B A" colour of the panel
protected void AddImage(string parent, string min, string max, string imageUrl, string colour = "1 1 1 1") =>
Container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiRawImageComponent
{
Url = imageUrl,
Sprite = "assets/content/textures/generic/fulltransparent.tga",
Color = colour
},
new CuiRectTransformComponent
{
AnchorMin = min,
AnchorMax = max
}
}
});
///
/// Add a CUI Button to the main elements container
///
/// CUI parent Id
/// The command to run if the button is click
/// Text to display
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// "R G B A" colour of the panel
/// Which element container to add this element to. The default is the foreground container
protected string AddButton(string parent, string command, string text = null, string min = "0 0", string max = "1 1", string colour = "0 0 0 0") =>
Container.Add(MakeButton(command, text, min, max, colour), parent);
public virtual void Dispose()
{
Close();
Container = null;
PrimaryPanel = null;
}
}
internal class CuiBase
{
protected CuiButton MakeButton(string command, string text = null, string min = "0 0", string max = "1 1",
string colour = "0 0 0 0") =>
new CuiButton
{
Button =
{
Command = command,
Color = colour
},
RectTransform =
{
AnchorMin = min,
AnchorMax = max
},
Text =
{
Text = text ?? string.Empty,
Align = TextAnchor.MiddleCenter,
FontSize = 11
}
};
protected CuiLabel MakeLabel(string text, int fontSize, TextAnchor alignment, string min = "0 0", string max = "1 1",
string colour = "1 1 1 1") => new CuiLabel
{
Text =
{
Text = text,
Align = alignment,
FontSize = fontSize,
Color = colour
},
RectTransform =
{
AnchorMin = min,
AnchorMax = max
}
};
protected CuiElement MakeLabelWithOutline(string text, int fontSize, TextAnchor alignment,
string min = "0 0", string max = "1 1", string textColour = "1 1 1 1", string outlineColour = "0.15 0.15 0.15 0.43", string distance = "1.1 -1.1", bool useGraphicAlpha = false)
{
var label = MakeLabel(text, fontSize, alignment, min, max, textColour);
return new CuiElement
{
Components =
{
label.Text,
label.RectTransform,
new CuiOutlineComponent
{
Color = outlineColour,
Distance = distance,
UseGraphicAlpha = useGraphicAlpha
}
}
};
}
protected CuiPanel MakePanel(string min, string max, string colour, bool cursorEnabled) => new CuiPanel
{
Image = { Color = colour },
RectTransform = { AnchorMin = min, AnchorMax = max },
CursorEnabled = cursorEnabled
};
protected CuiElement MakePanelWithOutline(string min, string max, string colour, bool cursorEnabled, string outlineColour = "0.15 0.15 0.15 0.43", string distance = "1.1 -1.1", bool useGraphicAlpha = false)
{
var panel = MakePanel(min, max, colour, cursorEnabled);
return new CuiElement
{
Components =
{
panel.RectTransform,
new CuiOutlineComponent
{
Color = outlineColour,
Distance = distance,
UseGraphicAlpha = useGraphicAlpha
}
}
};
}
}
#endregion
#region Data
///
/// The data handler for loading and saving data to disk
///
//[JsonConverter(typeof(DataConverter))]
class Data
{
///
/// The data for all the pipes
///
public PipeData[] PipeData { get; set; }
///
/// The data for all the container managers
///
public List ContainerData { get; set; }
///
/// Load syncPipes data from disk
///
public static void Load()
{
var data = Interface.Oxide.DataFileSystem.ReadObject(Instance.Name);
if (data != null)
{
Pipe.Load(data.PipeData);
ContainerManager.Load(data.ContainerData);
}
}
}
class DataStore1_0: MonoBehaviour
{
private static UnityEngine.Coroutine _coroutine;
private static bool _saving;
private static bool _loading;
private static GameObject _saverGameObject;
private static DataStore1_0 _dataStore;
private static DataStore1_0 DataStore
{
get
{
if (_dataStore == null)
{
_saverGameObject =
new GameObject($"{Instance.Name.ToLower()}-datastore-1-0");
_dataStore = _saverGameObject.AddComponent();
}
return _dataStore;
}
}
private static string _filename;
private static string Filename => _filename ?? (_filename = $"{Instance.Name}_v1-0");
private static string OldFilename => $"{Instance.Name} v1-0";
public static bool Save(bool backgroundSave = true)
{
try
{
if (_loading)
return false;
if (!backgroundSave && _saving)
{
if (_coroutine != null)
DataStore.StopCoroutine(_coroutine);
_saving = false;
}
else if (_saving)
return false;
try
{
_saving = true;
if (backgroundSave)
_coroutine = DataStore.StartCoroutine(DataStore.BufferedSave(Filename));
else
{
var enumerator = DataStore.BufferedSave(Filename);
while (enumerator.MoveNext()) { }
}
return true;
}
finally
{
_saving = false;
}
}
catch (Exception e)
{
Logger.Runtime.LogException(e, "DataStore1_0.Save");
_saving = false;
return false;
}
}
public static bool Load()
{
try
{
_loading = true;
var filename = Filename;
if (!Interface.Oxide.DataFileSystem.ExistsDatafile(Filename))
{
if (!Interface.Oxide.DataFileSystem.ExistsDatafile(OldFilename))
{
_loading = false;
return false;
}
filename = OldFilename;
}
_coroutine = DataStore.StartCoroutine(DataStore.BufferedLoad(filename));
return true;
}
catch (Exception e)
{
_loading = false;
Logger.Runtime.LogException(e, "DataStore1_0.Load");
return false;
}
}
class Converter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
try
{
var buffer = value as Buffer;
if (buffer == null) return;
writer.WriteStartObject();
writer.WritePropertyName("pipes");
writer.WriteStartArray();
for (int i = 0; i < buffer.Pipes.Count; i++)
writer.WriteRawValue(buffer.Pipes[i]);
writer.WriteEndArray();
writer.WritePropertyName("containers");
writer.WriteStartArray();
for (int i = 0; i < buffer.Containers.Count; i++)
writer.WriteRawValue(buffer.Containers[i]);
writer.WriteEndArray();
writer.WriteEndObject();
}
catch (Exception e)
{
Logger.Runtime.LogException(e, "DataStore1_0.Converter.WriteJson");
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
var buffer = new Loader();
try
{
while (reader.Read())
{
if (reader.TokenType == JsonToken.PropertyName)
{
switch ((string) reader.Value)
{
case "pipes":
reader.Read();
reader.Read();
while (reader.TokenType != JsonToken.EndArray)
{
var pipe = serializer.Deserialize(reader);
if (pipe.Validity == Pipe.Status.Success)
buffer.Pipes.Add(pipe);
else
Instance.Puts("Failed to read pipe {0}({1})",
pipe.DisplayName ?? pipe.Id.ToString(), pipe.OwnerId);
}
break;
case "containers":
reader.Read();
while (reader.Read() && reader.TokenType != JsonToken.EndArray)
{
var data = new ContainerManager.Data();
while (reader.Read() && reader.TokenType != JsonToken.EndObject)
{
if (reader.TokenType == JsonToken.PropertyName)
{
switch (reader.Value.ToString())
{
case "ci":
reader.Read();
uint.TryParse(reader.Value.ToString(),
out data.ContainerId);
break;
case "cs":
data.CombineStacks = reader.ReadAsBoolean() ?? true;
break;
case "dn":
data.DisplayName = reader.ReadAsString();
break;
case "ct":
data.ContainerType =
(ContainerType) reader.ReadAsInt32()
.GetValueOrDefault();
break;
}
}
}
buffer.Containers.Add(data);
}
break;
}
}
}
}
catch (Exception e)
{
Logger.Runtime.LogException(e, "DataStore1_0.Converter.ReadJson");
}
return buffer;
}
public override bool CanConvert(Type objectType)
{
return true;
}
}
[JsonConverter(typeof(Converter))]
class Buffer
{
public List Pipes { get; } = new List();
public List Containers { get; } = new List();
}
[JsonConverter(typeof(Converter))]
class Loader
{
public List Pipes { get; } = new List();
public List Containers { get; } = new List();
}
IEnumerator BufferedSave(string filename)
{
var sw = Stopwatch.StartNew();
yield return null;
Instance.Puts("Save v1.0 starting");
var buffer = new Buffer();
var pipeSnapshot = new List(Pipe.Pipes);
var containerSnapshot = new List(ContainerManager.ManagedContainers);
for (int i = 0; i < pipeSnapshot.Count; i++)
{
try
{
buffer.Pipes.Add(JsonConvert.SerializeObject(pipeSnapshot[i], Formatting.None));
}
catch (Exception e)
{
Logger.Runtime.LogException(e, "DataStore1_0.BufferedSave");
}
yield return null;
}
Instance.Puts("Saved {0} pipes", buffer.Pipes.Count);
for(int i = 0; i < containerSnapshot.Count; i++)
{
try
{
if (!containerSnapshot[i].HasAnyPipes) continue;
buffer.Containers.Add(JsonConvert.SerializeObject(containerSnapshot[i], Formatting.None));
}
catch (Exception e)
{
Logger.Runtime.LogException(e, "DataStore1_0.BufferedSave");
}
yield return null;
}
Instance.Puts("Saved {0} managers", buffer.Containers.Count);
Interface.Oxide.DataFileSystem.WriteObject(filename, buffer);
Interface.Oxide.DataFileSystem.GetDatafile($"{Instance.Name}").Clear();
Instance.Puts("Save v1.0 complete ({0}.{1:00}s)", sw.Elapsed.Seconds, sw.Elapsed.Milliseconds);
sw.Stop();
_saving = false;
yield return null;
}
IEnumerator BufferedLoad(string filename)
{
try
{
yield return null;
Instance.Puts("Load v1.0 starting");
var loader = Interface.Oxide.DataFileSystem.ReadObject(filename);
for (int i = 0; i < loader.Pipes.Count; i++)
{
loader.Pipes[i].Create();
yield return null;
}
Instance.Puts("Successfully loaded {0} pipes", loader.Pipes.Count);
ContainerManager.Load(loader.Containers);
Instance.Puts("Load v1.0 complete");
yield return null;
}
finally
{
_loading = false;
}
}
}
#endregion
#region EntityHooks
///
/// Hook: Used to prevent the lights from the pipes being picked up and displays a warning
///
/// Player trying to pick up an entity
/// entity to check to see if it is the lights from a pipe
/// false if the entity is the lights from a pipe
bool? CanPickupEntity(BasePlayer player, BaseEntity entity)
{
var lights = entity?.GetComponent();
if (lights == null) return null;
var playerHelper = PlayerHelper.Get(player);
playerHelper?.ShowOverlay(Overlay.CantPickUpLights);
OverlayText.Hide(player, 2f);
return false;
}
///
/// Hook: Used to ensure pipes are removed when a segment of the pipe is killed
///
/// Entity to check to see if it's a pipe segment
void OnEntityKill(BaseNetworkable entity) => entity?.GetComponent()?.Pipe?.Remove();
///
/// Hook: Used to ensure pies are removed when a segment of the pipe dies
///
/// Entity to check to see if it's a pipe segment
///
void OnEntityDeath(BaseCombatEntity entity, HitInfo info) => entity?.GetComponent()?.Pipe?.Remove();
///
/// Hook: Used to handle hits to the pipes or connected containers
///
/// Player hitting
/// Information about the hit
void OnHammerHit(BasePlayer player, HitInfo hit)
{
if (player == null || hit?.HitEntity == null)
return;
var playerHelper = PlayerHelper.Get(player);
var handled =
Handlers.HandleNamingContainerHit(playerHelper, hit.HitEntity) ||
Handlers.HandlePlacementContainerHit(playerHelper, hit.HitEntity) ||
Handlers.HandlePipeCopy(playerHelper, hit.HitEntity) ||
Handlers.HandlePipeRemove(playerHelper, hit.HitEntity) ||
Handlers.HandlePipeMenu(playerHelper, hit.HitEntity) ||
Handlers.HandleContainerManagerHit(playerHelper, hit.HitEntity);
}
///
/// New Hook: This allows other plugins to determine if the entity is a pipe
///
/// Entity to check to see if it a pipe
/// Only return true if the pipe is also running
/// True if the entity is a pipe segment (and if it is running)
private bool IsPipe(BaseEntity entity, bool checkRunning = false) => checkRunning ? entity?.GetComponent()?.enabled ?? false : entity?.GetComponent()?.Pipe != null;
///
/// New Hook: This allows other plugins to determine if the entity is a managed container.
///
/// Entity to check to see if it is a managed container
/// True if the entity is a managed container
private bool IsManagedContainer(BaseEntity entity) => entity?.GetComponent()?.HasAnyPipes ?? false;
#endregion
#region Filter
///
/// This represents the filter for a pipe.
/// It creates a virtual loot container with the correct items.
///
public class PipeFilter
{
///
/// All items in the virtual filter container
///
public List Items => _filterContainer.itemList;
// A list if players currently viewing the filter
private readonly List _playersInFilter = new List();
///
/// Ensure the filter is cleaned up and all players are disconnect from it when the container is destoryed.
///
~PipeFilter()
{
Kill();
}
///
/// Disconnect all players and empty the container when the filter is destroyed.
///
public void Kill()
{
ForceClosePlayers();
KillFilter();
}
///
/// Destroy the virtual storage container
///
private void KillFilter()
{
_filterContainer?.Kill();
_filterContainer = null;
ItemManager.DoRemoves();
}
///
/// force Close the filter loot screen for all players currently viewing it
///
private void ForceClosePlayers()
{
foreach (var player in _playersInFilter.ToArray())
ForceClosePlayer(player);
}
///
/// Force Close the filter loot screen for a specific player
///
/// Player to close the filter for
private void ForceClosePlayer(BasePlayer player)
{
player?.inventory.loot.Clear();
player?.inventory.loot.MarkDirty();
player?.inventory.loot.SendImmediate();
Closing(player);
}
///
/// Remove the player from the list of players in the filter
///
/// Player closing the menu
public void Closing(BasePlayer player)
{
if(player != null)
_playersInFilter.Remove(player);
}
///
/// Creates a virtual storage container with all the items from the pipe and limits it to the pipes filter capacity
///
/// Items to filter by
/// Maximum items allow for the curent pipe grade
/// The pipe this filter is attached to
public PipeFilter(List filterItems, int capacity, Pipe pipe)
{
_pipe = pipe;
_filterContainer = new ItemContainer
{
entityOwner = pipe.PrimarySegment,
isServer = true,
allowedContents = ItemContainer.ContentsType.Generic,
capacity = capacity,
maxStackSize = 1,
canAcceptItem = CanAcceptItem
};
_filterContainer.GiveUID();
for (var i = 0; i < capacity && i < filterItems.Count; i++)
ItemManager.CreateByItemID(filterItems[i]).MoveToContainer(_filterContainer);
}
///
/// Prevents the filter from taking an item from the player but adds a dummy item to the filter
///
/// Item to add
/// Stack position to place the item
/// False to the hook caller
private bool CanAcceptItem(Item item, int position)
{
// Checks if the item is in the list of items to add to the filter.
// If so return true to allow the item to be added.
if (_addItem.Contains(item))
{
_addItem.Remove(item);
return true;
}
// Check if the filter already has this item
if (_filterContainer.FindItemByItemID(item.info.itemid) == null)
{
// Add a dummy item to the list of items to add and then move it to the filter.
var filterItem = ItemManager.CreateByItemID(item.info.itemid);
_addItem.Add(filterItem);
filterItem.MoveToContainer(_filterContainer, position, false);
}
// Prevent the item being taken from the player
return false;
}
// List of dummy items to be added to the filter.
private readonly List _addItem = new List();
///
/// Upgrade the capacity of the filter.
/// Cannot be less than the previous capacity.
///
///
public void Upgrade(int newCapacity)
{
if (newCapacity < _filterContainer.capacity) return;
_filterContainer.capacity = newCapacity;
}
///
/// Open the filter as a loot box to the player
///
/// Player to show filter to
public void Open(PlayerHelper playerHelper)
{
var player = playerHelper.Player;
if (player == null)
return;
playerHelper.PipeFilter = this;
if (_playersInFilter.Contains(player) || !Active)
return;
_playersInFilter.Add(player);
player.inventory.loot.Clear();
player.inventory.loot.PositionChecks = false;
player.inventory.loot.entitySource = _pipe.PrimarySegment;
player.inventory.loot.itemSource = null;
player.inventory.loot.MarkDirty();
player.inventory.loot.AddContainer(_filterContainer);
player.inventory.loot.SendImmediate();
player.inventory.loot.useGUILayout = false;
player.ClientRPCPlayer(null, player, "RPC_OpenLootPanel", "generic_resizable");
}
private ItemContainer _filterContainer;
private readonly Pipe _pipe;
private bool Active => _filterContainer != null;
}
///
/// Hook: Close the players connection to the filter when they disconnect from the filter.
/// Remove the player from the players in filter list
///
/// Used to get the player in the filter
private void OnPlayerLootEnd(PlayerLoot playerLoot) => PlayerHelper.Get((BasePlayer)playerLoot.gameObject.ToBaseEntity())?.CloseFilter();
///
/// Hook: This is used to prevent the player from removing anything from the filter
///
/// Container being viewed
/// Item being removed
private void OnItemRemovedFromContainer(ItemContainer container, Item item)
{
if (container?.entityOwner?.GetComponent() != null)
item?.Remove();
}
///
/// Hook: This is used to prevent a filter item being added to an existing stack in the players inventory
///
/// Item being removed
/// Stack being added to
/// If the item can be stacked
private bool? CanStackItem(Item item, Item targetItem) => targetItem?.parent?.entityOwner?.GetComponent() != null ? (bool?)false : null;
#endregion
#region Handlers
///
/// This class holds all the handlers for various events that the player can carry out like hitting containers or pipes
/// They will all return true if they have handled the event
///
static class Handlers
{
///
/// This will handle if a container or pipe is hit whilst the player is in naming mode
///
/// Player Helper for the player invoking the hammer hit
/// The entity hit by the hammer
/// True indicates the hit was handled
public static bool HandleNamingContainerHit(PlayerHelper playerHelper, BaseEntity entity)
{
var containerManager = entity?.GetComponent();
var pipe = entity?.GetComponent()?.Pipe;
if (playerHelper.State != PlayerHelper.UserState.Naming)
return false;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
if (containerManager != null && containerManager.HasAnyPipes)
containerManager.DisplayName = playerHelper.NamingName;
else if (pipe != null)
pipe.DisplayName = playerHelper.NamingName;
else
{
playerHelper.ShowOverlay(Overlay.CannotNameContainer);
playerHelper.ShowNamingOverlay(2f);
return true;
}
playerHelper.StopNaming();
return true;
}
///
/// Handle a Hammer Hit on a Container when Placing.
///
/// Player Helper for the player invoking the hammer hit
/// The entity hit by the hammer
/// True indicates the hit was handled
public static bool HandlePlacementContainerHit(PlayerHelper playerHelper, BaseEntity entity)
{
if (playerHelper.State != PlayerHelper.UserState.Placing ||
playerHelper.Destination != null ||
entity.GetComponent() == null ||
entity.GetComponent() != null
)
return false;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
if (ContainerHelper.IsBlacklisted(entity))
{
playerHelper.ShowOverlay(Overlay.BlacklistedContainer);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
if (!playerHelper.HasContainerPrivilege(entity) || !playerHelper.CanBuild)
{
playerHelper.ShowOverlay(Overlay.NoPrivilegeToCreate);
playerHelper.ShowPlacingOverlay(2f);
}
if (!ContainerHelper.InMonument(entity))
{
playerHelper.ShowOverlay(Overlay.MonumentDenied);
playerHelper.ShowPlacingOverlay(2f);
}
else
{
if (playerHelper.Source == null)
{
playerHelper.Source = entity;
}
else
{
playerHelper.Destination = entity;
Pipe.TryCreate(playerHelper);
return true;
}
playerHelper.ShowPlacingOverlay();
}
return true;
}
///
/// Handle a hammer hit on a pipe whilst the player is in naming mode
///
/// Player Helper for the player invoking the hammer hit
/// The entity hit by the hammer
/// True indicates the hit was handled
public static bool HandlePipeCopy(PlayerHelper playerHelper, BaseEntity entity)
{
var pipe = entity?.GetComponent()?.Pipe;
if (playerHelper.State != PlayerHelper.UserState.Copying || pipe == null) return false;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
if (playerHelper.CanBuild)
{
if (playerHelper.CopyFrom == null)
playerHelper.CopyFrom = pipe;
else
pipe.CopyFrom(playerHelper.CopyFrom);
playerHelper.ShowCopyOverlay();
}
else
{
playerHelper.ShowOverlay(Overlay.NoPrivilegeToEdit);
OverlayText.Hide(playerHelper.Player, 3f);
}
return true;
}
///
/// Handle a hammer hit on a pipe whilst the player is in remove mode
///
/// Player Helper for the player invoking the hammer hit
/// The entity hit by the hammer
/// True indicates the hit was handled
public static bool HandlePipeRemove(PlayerHelper playerHelper, BaseEntity entity)
{
var pipe = entity?.GetComponent()?.Pipe;
if (playerHelper.State != PlayerHelper.UserState.Removing || pipe == null) return false;
pipe.Remove();
playerHelper.ShowRemoveOverlay();
return true;
}
///
/// Handles a hammer hit on a pipe whilst the player is not in any special mode
///
/// Player Helper for the player invoking the hammer hit
/// The entity hit by the hammer
/// True indicates the hit was handled
public static bool HandlePipeMenu(PlayerHelper playerHelper, BaseEntity entity)
{
var pipe = entity?.GetComponent()?.Pipe;
if (pipe == null || !pipe.CanPlayerOpen(playerHelper)) return false;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
pipe.OpenMenu(playerHelper);
return true;
}
///
/// Handles a hammer hit on a pipe container whilst the player is not in any special mode
///
/// Player Helper for the player invoking the hammer hit
/// The entity hit by the hammer
/// True indicates the hit was handled
public static bool HandleContainerManagerHit(PlayerHelper playerHelper, BaseEntity entity)
{
return false;
// var containerManager = entity?.GetComponent();
//
// if (containerManager == null || !containerManager.HasAnyPipes) return false;
// if (!playerHelper.IsUser)
// {
// playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
// OverlayText.Hide(playerHelper.Player, 2f);
// return false;
// }
// var container = entity as StorageContainer;
// //ToDo: Implement this...
// return true;
}
///
/// Handles any upgrades to ensure pipes are upgraded correctly when any pipe segments are upgraded
///
/// The entity hit by the hammer
/// Player Helper for the player invoking the hammer hit
/// The grade the structure has been upgraded to
/// True indicates the upgrade was handled
public static bool? HandlePipeUpgrade(BaseCombatEntity entity, PlayerHelper playerHelper, BuildingGrade.Enum grade)
{
var pipe = entity?.GetComponent()?.Pipe;
if (pipe == null || playerHelper == null) return null;
var maxUpgrade = playerHelper.MaxUpgrade;
if (!playerHelper.IsUser)
{
playerHelper.ShowOverlay(Overlay.NotAuthorisedOnSyncPipes);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
if(maxUpgrade != -1 && maxUpgrade < (int)grade)
{
playerHelper.ShowOverlay(Overlay.UpgradeLimitReached);
OverlayText.Hide(playerHelper.Player, 2f);
return false;
}
pipe.Upgrade(grade);
return null;
}
}
#endregion
#region Logging
public enum LogLevels
{
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Fatal = 5
}
class Logger
{
public static readonly Logger PipeLoader = new Logger("PipeLoadErrors", LogLevels.Error);
public static readonly Logger ContainerLoader = new Logger("ContainerLoadErrors", LogLevels.Error);
public static readonly Logger FindErrors = new Logger("FindErrors", LogLevels.Error);
public static readonly Logger Runtime = new Logger("Runtime", LogLevels.Info);
public Logger(string filename, LogLevels defaultLogLevel)
{
_filename = filename;
_defaultLogLevel = defaultLogLevel;
}
private readonly string _filename;
private readonly LogLevels _defaultLogLevel;
public void Log(string format, params object[] args)
{
Log(_defaultLogLevel, format, args);
}
public void Log(LogLevels logLevel, string format, params object[] args)
{
Instance.LogToFile(_filename, string.Format("[{0}]: {1}", logLevel, string.Format(format, args)), Instance);
}
public void LogSection(string section, string format, params object[] args)
{
LogSection(_defaultLogLevel, section, format, args);
}
public void LogSection(LogLevels logLevel, string section, string format, params object[] args)
{
Log(logLevel, "{0} - {1}", section, string.Format(format, args));
}
public void LogException(Exception e, string section = null)
{
Log(LogLevels.Error, e.Message);
if(section != null)
Log(LogLevels.Error, "Exception thrown in {0}", section);
Log(LogLevels.Error, e.Source);
Log(LogLevels.Error, e.StackTrace);
Instance.PrintError("Exception thrown. See log file for more details.");
}
}
#endregion
#region Menu
///
/// The Pipes Control Menu
///
public class PipeMenu
{
///
/// Pipe Menu Buttons
///
public enum Button
{
TurnOn,
TurnOff,
SetSingleStack,
SetMultiStack,
OpenFilter,
SwapDirection,
}
///
/// Pipe Menu Info Panel Labels
///
public enum InfoLabel
{
Title,
Owner,
FlowRate,
Material,
Length,
FilterCount,
FilterLimit,
FilterItems
}
///
/// Pipe Menu Control Labels
///
public enum ControlLabel
{
MenuTitle,
On,
Off,
OvenOptions,
QuarryOptions,
RecyclerOptions,
AutoStart,
AutoSplitter,
StackCount,
Status,
StackMode,
PipePriority,
Running,
Disabled,
SingleStack,
MultiStack,
UpgradeToFilter,
}
///
/// Pipe Menu Help Labels
///
public enum HelpLabel
{
FlowBar,
AutoStart,
FurnaceSplitter,
Status,
StackMode,
Priority,
SwapDirection,
Filter
}
///
/// Helper to make commands for button actions. It will automatically prefix the command prefix and append to pipe id.
///
/// The command name for the action to be performed (what appears after the dot)
/// All required for the command (the pipe id is added automatically as the first arg)
/// Fully formed string for the command to be run
private string MakeCommand(string commandName, params object[] args)
{
var command = $"{Instance.Name.ToLower()}.{commandName} {_pipe.Id} {string.Join(" ", args)}";
return command;
}
#region Standard Colours
private const string OnColour = "0.5 1 0.5 0.8";
private const string OnTextColour = "0.2 1 0.2 1";
private const string OffColour = "1 0.5 0.5 0.8";
private const string OffTextColour = "1 0.2 0.2 1";
private const string LabelColour = "0.5 1 1 1";
#endregion
private static readonly Vector2 Size = new Vector2(0.115f, 0.25f); //The main panel will be centered plus and minus these values to make the panel
private readonly CuiElementContainer _foregroundElementContainer = new CuiElementContainer(); //This element container holds the foreground controls
private readonly CuiElementContainer _backgroundElementContainer = new CuiElementContainer(); //This element container holds a panel that prevents screen mouse twitching when the foreground is refreshed
private readonly CuiElementContainer _helpElementContainer = new CuiElementContainer();
private string _foregroundPanel; // This panel contains all the foreground elements
private string _helpPanel; // This panel shows the help information
private readonly string _backgroundPanel; // This panel does not hold any elements and holds mouse focus when the foreground panel refreshes
private readonly Pipe _pipe; // The pipe that this menu is for. This is held onto to make refreshing easier.
private readonly PlayerHelper _playerHelper;// The player helper of the player this menu is being shown to
private bool _helpOpen; // Indicates if the help panel is open or not
///
/// Create a new Pipe Menu Instance for a player
///
/// Pipe to create a menu for
/// PlayerHelper to create the menu for
public PipeMenu(Pipe pipe, PlayerHelper playerHelper)
{
_backgroundPanel = _backgroundElementContainer.Add(new CuiPanel
{
Image = { Color = "0 0 0 0.95" },
RectTransform = {AnchorMin = "0 0", AnchorMax = "1 1"},
CursorEnabled = true
});
_pipe = pipe;
_playerHelper = playerHelper;
CreateForeground();
}
///
/// Create the menu and Info Panels
///
private void CreateForeground()
{
_foregroundElementContainer.Clear();
_foregroundPanel = AddPanel("Hud", "0 0", "1 1");
AddButton(_foregroundPanel, MakeCommand("closemenu"));
var mainPanel = AddPanel(_foregroundPanel, $"{0.5f - Size.x} {0.5f - Size.y}", $"{0.5f + Size.x} {0.5f + Size.y}");
var title = _playerHelper.GetPipeMenuControlLabel(ControlLabel.MenuTitle);
AddLabel(mainPanel, title, 32, TextAnchor.UpperCenter, "0 0.915", "0.99 1.05", colour: "1 1 1 1");
PipeVisualPanel(mainPanel);
StartablePanel(mainPanel);
ControlPanel(mainPanel);
InfoPanel();
AddButton(mainPanel, MakeCommand("menuhelp"), "?", "1 1.02", "1.1 1.1", "0.99 0.35 0.01 0.8");
}
///
/// Visual Representation of the pipe with from/to containers, speed and status (given by the background colour). Names can also be displayed if set.
///
/// Panel to add the controls to
private void PipeVisualPanel(string panel)
{
var pipeVisualPanel = AddPanel(panel, "0.01 0.7", "0.99 0.915", _pipe.IsEnabled ? OnColour : OffColour);
AddImage(pipeVisualPanel, "0.15 0.2", "0.35 0.9", _pipe.Source.IconUrl);
AddImage(pipeVisualPanel, "0.65 0.2", "0.85 0.9", _pipe.Destination.IconUrl);
AddLabel(pipeVisualPanel, _pipe.Source.ContainerManager.DisplayName ?? "", 10, TextAnchor.MiddleCenter, "0.11 0.01",
"0.49 0.35");
AddLabel(pipeVisualPanel, _pipe.Destination.ContainerManager.DisplayName ?? "", 10, TextAnchor.MiddleCenter,
"0.51 0.01", "0.89 0.35");
//ToDo: Add these buttons to navigate through the pipe system. But need the ContainerManager Menu first...
//AddButton(connectionPanel, "", "<", "0 0", "0.1 1", "0 0 0 0.5");
//AddButton(connectionPanel, "", ">", "0.9 0", "1 1", "0 0 0 0.5");
AddLabel(pipeVisualPanel, "".PadRight((int) _pipe.Grade + 1, '>'), 14, TextAnchor.MiddleCenter, "0 0.1", "1 0.9");
AddLabel(pipeVisualPanel, _pipe.DisplayName ?? "", 10, TextAnchor.UpperCenter, "0 0.7", "1 0.9");
}
///
/// Panel of options for startable items like Ovens, Recylcers and Quarries.
/// This panel also include Furnace Splitter options if applicable
///
/// Panel to add the controls to
private void StartablePanel(string panel)
{
var on = _playerHelper.GetPipeMenuControlLabel(ControlLabel.On);
var off = _playerHelper.GetPipeMenuControlLabel(ControlLabel.Off);
var turnOn = _playerHelper.GetMenuButton(Button.TurnOn);
var turnOff = _playerHelper.GetMenuButton(Button.TurnOff);
if (_pipe.CanAutoStart)
{
var furnacePanel = AddPanel(panel, "0.1 0.4", "0.9 0.7");
AddImage(furnacePanel, "0.75 0.9", "0.85 0.99", "http://i.imgur.com/BwJN0rt.png", "1 1 1 0.1");
var furnaceTitlePanel = AddPanel(furnacePanel, "0 0.70", "1 0.89", "1 1 1 0.3");
string furnaceTitle = null;
switch (_pipe.Destination.ContainerType)
{
case ContainerType.Oven:
furnaceTitle = _playerHelper.GetPipeMenuControlLabel(ControlLabel.OvenOptions);
break;
case ContainerType.Recycler:
furnaceTitle = _playerHelper.GetPipeMenuControlLabel(ControlLabel.RecyclerOptions);
break;
case ContainerType.FuelStorage:
furnaceTitle = _playerHelper.GetPipeMenuControlLabel(ControlLabel.QuarryOptions);
break;
}
AddLabel(furnaceTitlePanel, furnaceTitle, 12, TextAnchor.MiddleLeft, "0.02 0", "0.98 1");
var furnaceAutoStartPanel = AddPanel(furnacePanel, "0 0.45", "1 0.69", "1 1 1 0.3");
var furnaceAutoStartStatusPanel = AddPanel(furnaceAutoStartPanel, "0.02 0.2", "0.65 1", "0 0 0 0.6");
AddLabel(furnaceAutoStartStatusPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.AutoStart), 12, TextAnchor.MiddleLeft, "0.05 0", "0.6 1",
LabelColour);
AddLabel(furnaceAutoStartStatusPanel, _pipe.IsAutoStart ? on : off, 14, TextAnchor.MiddleCenter, "0.6 0",
"1 1", _pipe.IsAutoStart ? OnTextColour : OffTextColour);
AddButton(furnaceAutoStartPanel, MakeCommand("setpipeautostart", !_pipe.IsAutoStart), _pipe.IsAutoStart ? turnOff : turnOn, "0.7 0.2", "0.98 1",
_pipe.IsAutoStart ? OffColour : OnColour);
if (_pipe.Destination.ContainerType == ContainerType.Oven && Instance.FurnaceSplitter != null)
{
var furnaceSplitterPanel = AddPanel(furnacePanel, "0 0", "1 0.44", "1 1 1 0.3");
var furnaceSplitterStatusPanel = AddPanel(furnaceSplitterPanel, "0.02 0.1", "0.65 1", "0 0 0 0.6");
AddLabel(furnaceSplitterStatusPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.AutoSplitter), 12, TextAnchor.MiddleLeft, "0.05 0.5", "0.6 1",
LabelColour);
AddLabel(furnaceSplitterStatusPanel, _pipe.IsFurnaceSplitterEnabled ? on : off, 14, TextAnchor.MiddleCenter, "0.6 0.5", "1 1", _pipe.IsFurnaceSplitterEnabled ? OnTextColour : OffTextColour);
AddLabel(furnaceSplitterStatusPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.StackCount), 12, TextAnchor.MiddleLeft, "0.05 0.02", "0.6 0.5",
LabelColour);
AddButton(furnaceSplitterStatusPanel, MakeCommand("setpipefurnacestackcount", _pipe.FurnaceSplitterStacks-1), "-", "0.62 0.05", "0.7 0.45", "0 0 0 0.8");
AddLabel(furnaceSplitterStatusPanel, _pipe.FurnaceSplitterStacks.ToString(), 10, TextAnchor.MiddleCenter,
"0.7 0.02", "0.9 0.5");
AddButton(furnaceSplitterStatusPanel, MakeCommand("setpipefurnacestackcount", _pipe.FurnaceSplitterStacks + 1), "+", "0.9 0.05", "0.98 0.45", "0 0 0 0.8");
AddButton(furnaceSplitterPanel, MakeCommand("setpipefurnacestackenabled", !_pipe.IsFurnaceSplitterEnabled), _pipe.IsFurnaceSplitterEnabled ? turnOff : turnOn, "0.7 0.1", "0.98 1",
_pipe.IsFurnaceSplitterEnabled ? OffColour : OnColour);
}
}
}
///
/// General info about the pipe shown at the top right of the screen
///
private void InfoPanel()
{
var infoPanel = AddPanel(_foregroundPanel, $"0.85 0.5", $"0.995 0.99", "1 1 1 0.2");
var pipeInfo = _playerHelper.GetPipeMenuInfo(InfoLabel.Title);
AddLabel(infoPanel, pipeInfo, 24, TextAnchor.MiddleCenter, "0.01 0.9", "1 0.99", "0 0 0 0.5");
AddLabel(infoPanel, pipeInfo, 24, TextAnchor.MiddleCenter, "0 0.91", "0.99 1");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.Owner), 12, TextAnchor.MiddleLeft, "0.02 0.8", "0.35 0.85", LabelColour);
AddLabel(infoPanel, _pipe.OwnerName, 12, TextAnchor.MiddleLeft, "0.4 0.8", "1 0.85");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.FlowRate), 12, TextAnchor.MiddleLeft, "0.02 0.7", "0.35 0.75", LabelColour);
AddLabel(infoPanel,
$"{(decimal) _pipe.FlowRate / InstanceConfig.UpdateRate} item{(_pipe.FlowRate != 1 ? "s" : "")}/sec", 12,
TextAnchor.MiddleLeft, "0.4 0.7", "1 0.75");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.Material), 12, TextAnchor.MiddleLeft, "0.02 0.65", "0.35 0.7", LabelColour);
AddLabel(infoPanel, _pipe.Grade.ToString(), 12, TextAnchor.MiddleLeft, "0.4 0.65", "1 0.7");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.Length), 12, TextAnchor.MiddleLeft, "0.02 0.6", "0.35 0.65", LabelColour);
AddLabel(infoPanel, _pipe.Distance.ToString("0.00"), 12, TextAnchor.MiddleLeft, "0.4 0.6", "1 0.65");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.FilterCount), 12, TextAnchor.MiddleLeft, "0.02 0.5", "0.4 0.55", LabelColour);
AddLabel(infoPanel, _pipe.PipeFilter.Items.Count.ToString(), 12, TextAnchor.MiddleLeft, "0.4 0.5", "1 0.55");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.FilterLimit), 12, TextAnchor.MiddleLeft, "0.02 0.45", "0.4 0.5", LabelColour);
AddLabel(infoPanel, InstanceConfig.FilterSizes[(int) _pipe.Grade].ToString(), 12, TextAnchor.MiddleLeft, "0.4 0.45", "1 0.5");
AddLabel(infoPanel, _playerHelper.GetPipeMenuInfo(InfoLabel.FilterItems), 12, TextAnchor.MiddleLeft, "0.02 0.4", "0.4 0.45", LabelColour);
var items = new List();
for (int i = 0; i < _pipe.PipeFilter.Items.Count; i++)
items.Add(_pipe.PipeFilter.Items[i].info.displayName.translated);
AddLabel(infoPanel, string.Join(", ", items), 10,
TextAnchor.UpperLeft, "0.4 0.01", "1 0.45");
}
///
/// General pipe controls that appear on all pipes
///
/// Panel to add the controls to
private void ControlPanel(string panel)
{
var running = _playerHelper.GetPipeMenuControlLabel(ControlLabel.Running);
var disabled = _playerHelper.GetPipeMenuControlLabel(ControlLabel.Disabled);
var turnOn = _playerHelper.GetMenuButton(Button.TurnOn);
var turnOff = _playerHelper.GetMenuButton(Button.TurnOff);
var maxY = 0.683;
var height = 0.33;
var offset = 0.0;
if (_pipe.CanAutoStart)
offset += 0.163;
if (_pipe.Destination.ContainerType == ContainerType.Oven && Instance.FurnaceSplitter != null)
offset += 0.14;
maxY -= offset;
var minY = maxY - height;
var controlsPanel = AddPanel(panel, $"0.1 {minY}", $"0.9 {maxY}", "1 1 1 0.3");
var pipeStatusPanel = AddPanel(controlsPanel, "0.02 0.77", "0.65 0.95", "0 0 0 0.6");
AddLabel(pipeStatusPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.Status), 12, TextAnchor.MiddleLeft, "0.05 0.04", "0.6 0.96", LabelColour);
AddLabel(pipeStatusPanel, _pipe.IsEnabled ? running : disabled, 12, TextAnchor.MiddleCenter, "0.6 0.04",
"0.98 0.96", _pipe.IsEnabled ? OnTextColour : OffTextColour);
AddButton(controlsPanel, MakeCommand("setPipeState", !_pipe.IsEnabled), _pipe.IsEnabled ? turnOff : turnOn,
"0.7 0.77", "0.98 0.95", _pipe.IsEnabled ? OffColour : OnColour);
var stackStatusPanel = AddPanel(controlsPanel, "0.02 0.52", "0.65 0.74", "0 0 0 0.6");
AddLabel(stackStatusPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.StackMode), 12, TextAnchor.MiddleLeft, "0.05 0.04", "0.6 0.96", LabelColour);
AddLabel(stackStatusPanel, _pipe.IsMultiStack ? _playerHelper.GetPipeMenuControlLabel(ControlLabel.MultiStack) : _playerHelper.GetPipeMenuControlLabel(ControlLabel.SingleStack), 10, TextAnchor.MiddleCenter,
"0.6 0.05", "0.98 0.96");
AddButton(controlsPanel, MakeCommand("setpipemultistack", !_pipe.IsMultiStack), _pipe.IsMultiStack ? _playerHelper.GetMenuButton(Button.SetSingleStack) : _playerHelper.GetMenuButton(Button.SetMultiStack), "0.7 0.52", "0.98 0.74",
"0 0 0 0.9");
var priorityStatusPanel = AddPanel(controlsPanel, "0.02 0.31", "0.98 0.49", "0 0 0 0.6");
AddLabel(priorityStatusPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.PipePriority), 12, TextAnchor.MiddleLeft, "0.05 0.04", "0.5 0.96", LabelColour);
AddLabel(priorityStatusPanel, _playerHelper.GetPipePriorityText(_pipe.Priority), 10, TextAnchor.MiddleCenter, "0.6 0.05", "0.88 0.96");
AddButton(priorityStatusPanel, MakeCommand("changepriority", -1), "<", "0.5 0.1", "0.6 0.9", "0 0 0 0.8");
AddButton(priorityStatusPanel, MakeCommand("changepriority", +1), ">", "0.88 0.1", "0.98 0.9", "0 0 0 0.8");
AddButton(controlsPanel, MakeCommand("swappipedirection"), _playerHelper.GetMenuButton(Button.SwapDirection), "0.02 0.05", "0.475 0.25", "0 0 0 0.9");
if (_pipe.FilterCapacity > 0)
AddButton(controlsPanel, MakeCommand("openpipefilter"), _playerHelper.GetMenuButton(Button.OpenFilter), "0.525 0.05", "0.98 0.25",
"0 0 0 0.9");
else
AddLabel(controlsPanel, _playerHelper.GetPipeMenuControlLabel(ControlLabel.UpgradeToFilter), 10, TextAnchor.MiddleCenter, "0.525 0.05", "0.98 0.25",
"1 1 1 0.9");
}
///
/// Open the menu by creating the background element that holds cursor focus and the foreground elements that can be refreshed
///
public void Open()
{
CuiHelper.AddUi(_playerHelper.Player, _backgroundElementContainer);
CuiHelper.AddUi(_playerHelper.Player, _foregroundElementContainer);
_playerHelper.Menu = this;
}
///
/// Redraw the foreground elements to update the screen. Leave the background to prevent mouse flicker.
///
public void Refresh()
{
CuiHelper.DestroyUi(_playerHelper.Player, _foregroundPanel);
CreateForeground();
CuiHelper.AddUi(_playerHelper.Player, _foregroundElementContainer);
}
///
/// Close the foreground and background elements and clear the screen
///
///
public void Close(PlayerHelper playerHelper)
{
playerHelper.Menu = null;
CuiHelper.DestroyUi(_playerHelper.Player, _foregroundPanel);
CuiHelper.DestroyUi(_playerHelper.Player, _backgroundPanel);
if (_helpPanel != null)
CuiHelper.DestroyUi(_playerHelper.Player, _helpPanel);
}
///
/// Add a CUI Panel to the main elements container
///
/// Cui parent Id
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// "R G B A" colour of the panel
/// Enable Cursor interaction with this panel
/// Panel Id. Used as parent input for other CUI elements
/// Which element container to add this element to. The default is the foreground container
string AddPanel(string parent, string min, string max, string colour = "0 0 0 0", bool cursorEnabled = true, CuiElementContainer elementContainer = null) =>
(elementContainer ?? _foregroundElementContainer).Add(new CuiPanel
{
Image = {Color = colour},
RectTransform = {AnchorMin = min, AnchorMax = max},
CursorEnabled = cursorEnabled
}, parent);
///
/// Add a CUI Label to the main elements container
///
/// CUI parent Id
/// Text to display
/// Text font size
/// Text Alignment
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// "R G B A" colour of the panel
/// Which element container to add this element to. The default is the foreground container
void AddLabel(string parent, string text, int fontSize, TextAnchor alignment, string min = "0 0", string max = "1 1", string colour = "1 1 1 1", CuiElementContainer elementContainer = null) =>
(elementContainer ?? _foregroundElementContainer).Add(new CuiLabel
{
Text =
{
Text = text,
Align = alignment,
FontSize = fontSize,
Color = colour
},
RectTransform =
{
AnchorMin = min,
AnchorMax = max
}
}, parent);
///
/// Add the CUI Element with an image to the main elements container
///
/// CUI parent Id
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// Url of the image to show
/// "R G B A" colour of the panel
/// Which element container to add this element to. The default is the foreground container
void AddImage(string parent, string min, string max, string imageUrl, string colour = "1 1 1 1", CuiElementContainer elementContainer = null) =>
(elementContainer ?? _foregroundElementContainer).Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiRawImageComponent
{
Url = imageUrl,
Sprite = "assets/content/textures/generic/fulltransparent.tga",
Color = colour
},
new CuiRectTransformComponent
{
AnchorMin = min,
AnchorMax = max
}
}
});
///
/// Add a CUI Button to the main elements container
///
/// CUI parent Id
/// The command to run if the button is click
/// Text to display
/// Minimum coordinates of the panel (bottom left)
/// Maximum coordinates of the panel (top right)
/// "R G B A" colour of the panel
/// Which element container to add this element to. The default is the foreground container
void AddButton(string parent, string command, string text = null, string min = "0 0", string max = "1 1", string colour = "0 0 0 0", CuiElementContainer elementContainer = null) =>
(elementContainer ?? _foregroundElementContainer).Add(new CuiButton
{
Button =
{
Command = command,
Color = colour
},
RectTransform =
{
AnchorMin = min,
AnchorMax = max
},
Text =
{
Text = text ?? string.Empty,
Align = TextAnchor.MiddleCenter,
FontSize = 11
}
}, parent);
///
/// Toggle the help panels on and off. Ensuring that the foreground panel as redrawn over the top.
///
public void ToggleHelp()
{
if(_helpPanel == null)
CreateHelpPanel();
CuiHelper.DestroyUi(_playerHelper.Player, _helpPanel);
if (!_helpOpen)
{
CuiHelper.AddUi(_playerHelper.Player, _helpElementContainer);
CuiHelper.DestroyUi(_playerHelper.Player, _foregroundPanel);
CuiHelper.AddUi(_playerHelper.Player, _foregroundElementContainer);
}
_helpOpen = !_helpOpen;
}
///
/// Creates the help panel for the menu. This only needs to be created once per menu and can just be shown or hidden if the user needs it.
///
private void CreateHelpPanel()
{
_helpPanel = AddPanel("Hud", "0 0", "1 1", elementContainer: _helpElementContainer);
var flowBarPanel = AddPanel(_helpPanel, "0.05 0.6", "0.37 0.71", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(flowBarPanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.FlowBar), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
var controlPanelTop = 0.59;
if (_pipe.Destination.CanAutoStart)
{
var autoStartPanel = AddPanel(_helpPanel, "0.05 0.525", "0.39 0.585", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(autoStartPanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.AutoStart), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
controlPanelTop -= 0.08;
if (_pipe.Destination.ContainerType == ContainerType.Oven && Instance.FurnaceSplitter != null)
{
var splitterPlanel = AddPanel(_helpPanel, "0.05 0.45", "0.39 0.5225", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(splitterPlanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.FurnaceSplitter), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
controlPanelTop -= 0.07;
}
}
var controlPanelBottom = controlPanelTop - 0.34;
var controlsPanel = AddPanel(_helpPanel, $"0.05 {controlPanelBottom}", $"0.39 {controlPanelTop}", "0 0 0 0", elementContainer: _helpElementContainer);
var statusPanel = AddPanel(controlsPanel, "0 0.88", "1 1", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(statusPanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.Status), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
var stackModePanel = AddPanel(controlsPanel, "0 0.75", "1 0.87", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(stackModePanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.StackMode), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
var priorityPanel = AddPanel(controlsPanel, "0 0.62", "1 0.74", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(priorityPanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.Priority), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
var swapDirectionPanel = AddPanel(controlsPanel, "0.6 0.2", "1.31 0.45", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(swapDirectionPanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.SwapDirection), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
var filterPanel = AddPanel(controlsPanel, "1.34 0.2", "2.05 0.45", "0.99 0.35 0.01 0.5", elementContainer: _helpElementContainer);
AddLabel(filterPanel, _playerHelper.GetPipeMenuHelpLabel(HelpLabel.Filter), 10, TextAnchor.MiddleLeft, "0.02 0.01", "0.98 0.99", elementContainer: _helpElementContainer);
}
}
#endregion
#region Messages
// All enums that need chat command substitution
#pragma warning disable CS0649
private static Dictionary _chatCommands;
// All enums that need binding command substitution
private static Dictionary _bindingCommands;
// All enums that have a message type (mainly for overlay text)
private static Dictionary _messageTypes;
#pragma warning restore CS0649
///
/// Message type for helping with overlay messages
///
public enum MessageType
{
Info,
Success,
Warning,
Error
}
///
/// All messages sent to the players chat screen
///
public enum Chat
{
PlacingBindingHint,
Title,
Commands,
PipeMenuInstructions,
UpgradePipes,
StatsLimited,
StatsUnlimited
}
///
/// Helper for localizations of text to players
///
static class LocalizationHelpers
{
internal static Dictionary FallBack { get; set; }
///
/// Get the correct message for a player from a specific enum
/// It will automatically inject any binding or chat command text when needed
/// It will strip off and \r characters as these become visible in game
///
/// The enum key for this message
/// The player to get the message for
/// Any args needed for substitution of this message
/// The message for the enum
public static string Get(Enum key, BasePlayer player, params object[] args)
{
var argsList = new List