using System; using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using System.Globalization; namespace Barotrauma.Items.Components { partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable { private readonly struct EventData : IEventData { public readonly CustomInterfaceElement BtnElement; public EventData(CustomInterfaceElement btnElement) { BtnElement = btnElement; } } class CustomInterfaceElement : ISerializableEntity { public enum InputTypeOption { Number, Text, Button, TickBox } public bool ContinuousSignal; public bool State; public string ConnectionName; public Connection Connection; [Serialize("", IsPropertySaveable.No, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } [Serialize("1", IsPropertySaveable.No, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] public string Signal { get; set; } public Identifier PropertyName { get; } public Identifier TargetItemComponent { get; } public bool TargetOnlyParentProperty { get; } public string NumberInputMin { get; } public string NumberInputMax { get; } public string NumberInputStep { get; } public int NumberInputDecimalPlaces { get; } public int MaxTextLength { get; } public const string DefaultNumberInputMin = "0", DefaultNumberInputMax = "99", DefaultNumberInputStep = "1"; public const int DefaultNumberInputDecimalPlaces = 0; public InputTypeOption InputType { get; } public NumberType? NumberType { get; } public bool HasPropertyName { get; } public bool ShouldSetProperty { get; set; } /// /// By default, the elements in the interface only set values of the item or send signals. /// This can be used to make them additionally work the other way around, periodically getting the current value of the property from the item and refreshing the UI. /// public float GetValueInterval { get; set; } = -1.0f; public float GetValueTimer; public string Name => "CustomInterfaceElement"; public Dictionary SerializableProperties { get; set; } public List StatusEffects = new List(); /// /// Pass the parent component to the constructor to access the serializable properties /// for elements which change property values. /// public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent, InputTypeOption inputType) { Label = element.GetAttributeString("text", ""); ConnectionName = element.GetAttributeString("connection", ""); PropertyName = element.GetAttributeIdentifier("propertyname", Identifier.Empty); TargetItemComponent = element.GetAttributeIdentifier("targetitemcomponent", Identifier.Empty); TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); NumberInputMin = element.GetAttributeString("min", DefaultNumberInputMin); NumberInputMax = element.GetAttributeString("max", DefaultNumberInputMax); NumberInputStep = element.GetAttributeString("step", DefaultNumberInputStep); NumberInputDecimalPlaces = element.GetAttributeInt("decimalplaces", DefaultNumberInputDecimalPlaces); MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue); GetValueInterval = element.GetAttributeFloat(nameof(GetValueInterval), -1.0f); InputType = inputType; HasPropertyName = !PropertyName.IsEmpty; if (HasPropertyName) { if (inputType == InputTypeOption.Number) { string numberType = element.GetAttributeString("numbertype", string.Empty); switch (numberType) { case "f": case "float": NumberType = Barotrauma.NumberType.Float; break; case "int": case "integer": default: // backwards compatibility NumberType = Barotrauma.NumberType.Int; break; } } } if (element.GetAttribute("signal") is XAttribute attribute) { Signal = attribute.Value; ShouldSetProperty = HasPropertyName; } else if (HasPropertyName && parent != null) { parent.SetSignalToPropertyValue(this); } else { Signal = "1"; } foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")")); } } } } private string[] labels; [Serialize("", IsPropertySaveable.Yes, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] public string Labels { get { return string.Join(",", labels); } set { if (value == null) { return; } if (customInterfaceElementList.Count > 0) { string[] splitValues = value == "" ? Array.Empty() : value.Split(','); UpdateLabels(splitValues); } } } private string[] signals; [Serialize("", IsPropertySaveable.Yes, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.", alwaysUseInstanceValues: true)] public string Signals { //use semicolon as a separator because comma may be needed in the signals (for color or vector values for example) //kind of hacky, we should probably add support for (string) arrays to SerializableEntityEditor so this wouldn't be needed get { return signals == null ? string.Empty : string.Join(";", signals); } set { if (value == null) { return; } if (customInterfaceElementList.Count > 0) { string[] splitValues = value == "" ? Array.Empty() : value.Split(';'); UpdateSignals(splitValues); } } } private bool[] elementStates; [Serialize("", IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)] public string ElementStates { get { return elementStates == null ? string.Empty : string.Join(",", elementStates); } set { if (value == null) { return; } if (customInterfaceElementList.Count > 0) { string[] splitValues = value == "" ? Array.Empty() : value.Split(','); for (int i = 0; i < customInterfaceElementList.Count && i < splitValues.Length; i++) { if (!bool.TryParse(splitValues[i], out bool val)) { continue; } customInterfaceElementList[i].State = val; #if CLIENT if (uiElements != null && i < uiElements.Count && uiElements[i] is GUITickBox tickBox) { tickBox.Selected = val; } #endif } } } } [Serialize(false, IsPropertySaveable.Yes)] public bool ShowInsufficientPowerWarning { get; set; } private readonly List customInterfaceElementList = new List(); public CustomInterface(Item item, ContentXElement element) : base(item, element) { foreach (var subElement in element.Elements()) { bool continuousSignalByDefault = false; CustomInterfaceElement.InputTypeOption inputType = CustomInterfaceElement.InputTypeOption.Number; switch (subElement.Name.ToString().ToLowerInvariant()) { case "button": inputType = CustomInterfaceElement.InputTypeOption.Button; continuousSignalByDefault = false; break; case "textbox": inputType = CustomInterfaceElement.InputTypeOption.Text; continuousSignalByDefault = false; break; case "integerinput": // backwards compatibility case "numberinput": inputType = CustomInterfaceElement.InputTypeOption.Number; continuousSignalByDefault = false; break; case "tickbox": inputType = CustomInterfaceElement.InputTypeOption.TickBox; //the default behavior of tickboxes is different for mainly backwards compatibility reasons //(e.g. keeps sending a true/false signal depending on the state of the tickbox, while the others send a signal when the value changes) continuousSignalByDefault = true; break; default: continue; } var ciElement = new CustomInterfaceElement(item, subElement, this, inputType) { ContinuousSignal = subElement.GetAttributeBool(nameof(CustomInterfaceElement.ContinuousSignal), def: continuousSignalByDefault) }; if (string.IsNullOrEmpty(ciElement.Label)) { ciElement.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal == ciElement.ContinuousSignal); } customInterfaceElementList.Add(ciElement); IsActive |= ciElement.ContinuousSignal || ciElement.GetValueInterval > 0.0f; } InitProjSpecific(); //load these here to ensure the UI elements (created in InitProjSpecific) are up-to-date Labels = element.GetAttributeString("labels", ""); Signals = element.GetAttributeString("signals", ""); ElementStates = element.GetAttributeString("elementstates", ""); } private void UpdateLabels(string[] newLabels) { labels = new string[customInterfaceElementList.Count]; for (int i = 0; i < labels.Length; i++) { labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label; customInterfaceElementList[i].Label = labels[i]; } UpdateLabelsProjSpecific(); } private void UpdateSignals(string[] newSignals) { signals = new string[customInterfaceElementList.Count]; for (int i = 0; i < customInterfaceElementList.Count; i++) { var element = customInterfaceElementList[i]; if (i < newSignals.Length) { var newSignal = newSignals[i]; signals[i] = newSignal; element.ShouldSetProperty = element.Signal != newSignal; element.Signal = newSignal; } else { signals[i] = element.Signal; } if (element.HasPropertyName && element.ShouldSetProperty) { SetPropertyValueToSignal(element); customInterfaceElementList[i].ShouldSetProperty = false; } } UpdateSignalsProjSpecific(); } private void SetPropertyValueToSignal(CustomInterfaceElement element) { if (element.TargetOnlyParentProperty) { if (SerializableProperties.ContainsKey(element.PropertyName)) { SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal); } } else { foreach (var po in item.AllPropertyObjects) { if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; } if (!element.TargetItemComponent.IsEmpty && po.Name != element.TargetItemComponent) { continue; } po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal); } } } private void SetSignalToPropertyValue(CustomInterfaceElement element) { if (element.TargetOnlyParentProperty) { if (SerializableProperties.ContainsKey(element.PropertyName)) { element.Signal = SerializableProperties[element.PropertyName].GetValue(this)?.ToString(); } } else { foreach (ISerializableEntity e in item.AllPropertyObjects) { if (!e.SerializableProperties.ContainsKey(element.PropertyName)) { continue; } if (!element.TargetItemComponent.IsEmpty && e.Name != element.TargetItemComponent) { continue; } element.Signal = e.SerializableProperties[element.PropertyName].GetValue(e)?.ToString(); break; } } } public override void OnItemLoaded() { foreach (CustomInterfaceElement ciElement in customInterfaceElementList) { ciElement.Connection = item.Connections?.FirstOrDefault(c => c.Name == ciElement.ConnectionName); } #if SERVER //make sure the clients know about the states of the checkboxes and text fields if (customInterfaceElementList.Any()) { CoroutineManager.Invoke(() => { if (item.FullyInitialized && !item.Removed) { item.CreateServerEvent(this); } }, delay: 0.1f); } #endif } partial void UpdateLabelsProjSpecific(); partial void UpdateSignalsProjSpecific(); partial void InitProjSpecific(); private void ButtonClicked(CustomInterfaceElement btnElement) { if (btnElement == null) return; if (btnElement.Connection != null) { item.SendSignal(new Signal(btnElement.Signal, 0, null, item), btnElement.Connection); } foreach (StatusEffect effect in btnElement.StatusEffects) { item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character); } } private void TickBoxToggled(CustomInterfaceElement tickBoxElement, bool state) { if (tickBoxElement == null) { return; } tickBoxElement.State = state; tickBoxElement.Signal = state.ToString(); if (!tickBoxElement.ContinuousSignal) { SetPropertyValueToSignal(tickBoxElement); } } private void TextChanged(CustomInterfaceElement textElement, string text) { if (textElement == null) { return; } textElement.Signal = text; SetPropertyValueToSignal(textElement); } private void ValueChanged(CustomInterfaceElement numberInputElement, int value) { if (numberInputElement == null) { return; } numberInputElement.Signal = value.ToString(); SetPropertyValueToSignal(numberInputElement); foreach (StatusEffect effect in numberInputElement.StatusEffects) { item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character); } } private void ValueChanged(CustomInterfaceElement numberInputElement, float value) { if (numberInputElement == null) { return; } numberInputElement.Signal = value.ToString(); SetPropertyValueToSignal(numberInputElement); } public override void Update(float deltaTime, Camera cam) { foreach (CustomInterfaceElement ciElement in customInterfaceElementList) { if (ciElement.GetValueInterval > 0.0f) { ciElement.GetValueTimer -= deltaTime; if (ciElement.GetValueTimer <= 0.0f) { SetSignalToPropertyValue(ciElement); ciElement.GetValueTimer = ciElement.GetValueInterval; } } if (!ciElement.ContinuousSignal && ciElement.PropertyName != "Voltage") { continue; } //TODO: allow changing output when a tickbox is not selected if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null) { item.SendSignal(new Signal(ciElement.State ? ciElement.Signal : "0", source: item), ciElement.Connection); } foreach (StatusEffect effect in ciElement.StatusEffects) { item.ApplyStatusEffect(effect, ciElement.State ? ActionType.OnUse : ActionType.OnSecondaryUse, 1.0f, null, null, null, true, false); } } } public override void UpdateBroken(float deltaTime, Camera cam) { //CustomInterface works even when broken (it should be possible to tick the checkboxes and change values, //it's up to the other components to work or not work depending on whether the item is broken) Update(deltaTime, cam); } public override XElement Save(XElement parentElement) { labels = customInterfaceElementList.Select(ci => ci.Label).ToArray(); signals = customInterfaceElementList.Select(ci => ci.Signal).ToArray(); elementStates = customInterfaceElementList.Select(ci => ci.State).ToArray(); return base.Save(parentElement); } private static bool TryParseFloatInvariantCulture(string s, out float f) { return float.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out f); } } }