using Microsoft.Xna.Framework; using System; using Barotrauma.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma.CharacterEditor { class Wizard { // Ragdoll data private Identifier name; private bool isHumanoid; private bool canEnterSubmarine = true; private bool canWalk; private string texturePath; private string xmlPath; private ContentPackage contentPackage; private Dictionary limbXElements = new Dictionary(); private List limbGUIElements = new List(); private List jointXElements = new List(); private List jointGUIElements = new List(); public bool IsCopy { get; private set; } public CharacterParams SourceCharacter { get; private set; } public RagdollParams SourceRagdoll { get; private set; } public IEnumerable SourceAnimations { get; private set; } public void CopyExisting(CharacterParams character, RagdollParams ragdoll, IEnumerable animations) { IsCopy = true; SourceCharacter = character; SourceRagdoll = ragdoll; SourceAnimations = animations; name = character.SpeciesName; isHumanoid = character.Humanoid; canEnterSubmarine = ragdoll.CanEnterSubmarine; canWalk = ragdoll.CanWalk; texturePath = ragdoll.Texture; if (string.IsNullOrEmpty(texturePath) && name != CharacterPrefab.HumanSpeciesName) { texturePath = ragdoll.Limbs.FirstOrDefault()?.GetSprite().Texture; } } public static Wizard instance; public static Wizard Instance { get { if (instance == null) { instance = new Wizard(); } return instance; } } public static LocalizedString GetCharacterEditorTranslation(string text) => CharacterEditorScreen.GetCharacterEditorTranslation(text); public void Reset() { CharacterView.Get().Release(); RagdollView.Get().Release(); instance = null; } public enum Tab { None, Character, Ragdoll } private View activeView; private Tab currentTab; public void SelectTab(Tab tab) { currentTab = tab; activeView?.Box.Close(); switch (currentTab) { case Tab.Character: activeView = CharacterView.Get(); break; case Tab.Ragdoll: activeView = RagdollView.Get(); break; case Tab.None: default: Reset(); break; } } public void AddToGUIUpdateList() { activeView?.Box.AddToGUIUpdateList(); } public void CreateCharacter(XElement ragdollElement, XElement characterElement = null, IEnumerable animations = null) { if (CharacterPrefab.Find(p => p.Identifier == name) != null) { bool isSamePackage = contentPackage.GetFiles().Any(f => Path.GetFileNameWithoutExtension(f.Path.Value) == name); LocalizedString verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; msgBox.Buttons[0].OnClicked = (_, userdata) => { msgBox.Close(); if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) { GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name.Value), GUIStyle.Green, font: GUIStyle.Font); } Wizard.Instance.SelectTab(Tab.None); return true; }; //msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked = (_, userdata) => { msgBox.Close(); return true; }; } else { if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) { GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name.Value), GUIStyle.Green, font: GUIStyle.Font); } Wizard.Instance.SelectTab(Tab.None); } } private class CharacterView : View { private static CharacterView instance; public static CharacterView Get() => Get(ref instance); public override void Release() => instance = null; protected override GUIMessageBox Create() { var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new LocalizedString[] { TextManager.Get("Cancel"), IsCopy ? TextManager.Get("Create") : TextManager.Get("Next") }, new Vector2(0.65f, 0.9f)); box.Header.Font = GUIStyle.LargeFont; box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = 20; int elementSize = 30; var frame = new GUIFrame(new RectTransform(new Point(box.Content.Rect.Width - (int)(40 * GUI.xScale), box.Content.Rect.Height - (int)(50 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), style: null, color: ParamsEditor.Color) { CanBeFocused = false }; var topGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 1), frame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 2 }; var fields = new List(); GUITextBox texturePathElement = null; GUITextBox xmlPathElement = null; GUIDropDown contentPackageDropDown = null; bool updateTexturePath = !IsCopy; bool isTextureSelected = false; void UpdatePaths() { string pathBase = $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; XMLPath = $"{pathBase}.xml"; xmlPathElement.Text = XMLPath; if (updateTexturePath) { TexturePath = $"{pathBase}.png"; texturePathElement.Text = TexturePath; } } for (int i = 0; i < 7; i++) { var mainElement = new GUIFrame(new RectTransform(new Point(topGroup.RectTransform.Rect.Width, elementSize), topGroup.RectTransform), style: null, color: Color.Gray * 0.25f); fields.Add(mainElement); switch (i) { case 0: new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("Name")); var nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name.Value ?? GetCharacterEditorTranslation("DefaultName").Value) { CaretColor = Color.White }; string ProcessText(string text) => text.RemoveWhitespace().CapitaliseFirstInvariant(); Name = ProcessText(nameField.Text).ToIdentifier(); nameField.OnTextChanged += (tb, text) => { Name = ProcessText(text).ToIdentifier(); UpdatePaths(); return true; }; break; case 1: var label = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("IsHumanoid")); var tickBox = new GUITickBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), string.Empty) { Selected = IsHumanoid, Enabled = !IsCopy, OnSelected = (tB) => IsHumanoid = tB.Selected }; if (!tickBox.Enabled) { label.TextColor *= 0.6f; } break; case 2: var l = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("CanEnterSubmarines")); var t = new GUITickBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), string.Empty) { Selected = CanEnterSubmarine, Enabled = !IsCopy, OnSelected = (tB) => CanEnterSubmarine = tB.Selected }; if (!t.Enabled) { l.TextColor *= 0.6f; } break; case 3: var lbl = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("CanWalk")); var txt = new GUITickBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), string.Empty) { Selected = CanWalk, Enabled = !IsCopy, OnSelected = (tB) => CanWalk = tB.Selected }; if (!txt.Enabled) { lbl.TextColor *= 0.6f; } break; case 4: new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("ConfigFileOutput")); xmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), string.Empty) { Text = XMLPath, CaretColor = Color.White }; xmlPathElement.OnTextChanged += (tb, text) => { XMLPath = text; return true; }; break; case 5: { new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), GetCharacterEditorTranslation("TexturePath")); var rightContainer = new GUIFrame(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), style: null); texturePathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1.0f), rightContainer.RectTransform, Anchor.CenterLeft), string.Empty) { Text = TexturePath, CaretColor = Color.White, }; texturePathElement.OnTextChanged += (tb, text) => { updateTexturePath = false; TexturePath = text; return true; }; LocalizedString title = GetCharacterEditorTranslation("SelectTexture"); new GUIButton(new RectTransform(new Vector2(0.3f / texturePathElement.RectTransform.RelativeSize.X, 1.0f), texturePathElement.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), title, style: "GUIButtonSmall") { OnClicked = (button, data) => { FileSelection.OnFileSelected = (file) => { string relativePath = Path.GetRelativePath(Environment.CurrentDirectory, Path.GetFullPath(file)); if (relativePath.StartsWith(ContentPackage.LocalModsDir)) { string[] pathSplit = relativePath.Split('/', '\\'); string modDirName = $"{ContentPackage.LocalModsDir}/{pathSplit[1]}"; string selectedModDir = (contentPackageDropDown.ListBox.SelectedData as ContentPackage)?.Dir.CleanUpPathCrossPlatform(correctFilenameCase: false) ?? ""; if (modDirName == selectedModDir) { relativePath = ContentPath.ModDirStr + "/" + string.Join("/", pathSplit[2..]); } else { relativePath = string.Format(ContentPath.OtherModDirFmt, pathSplit[1]) + "/" + string.Join("/", pathSplit[2..]); } } string destinationPath = relativePath; //copy file to XML path if it's not located relative to the game's files if (relativePath.StartsWith("..") || Path.GetPathRoot(Environment.CurrentDirectory) != Path.GetPathRoot(file)) { destinationPath = Path.Combine(Path.GetDirectoryName(XMLPath), Path.GetFileName(file)); string destinationDir = Path.GetDirectoryName(destinationPath); if (!Directory.Exists(destinationDir)) { Directory.CreateDirectory(destinationDir); } if (!File.Exists(destinationPath)) { File.Copy(file, Path.GetFullPath(destinationPath), overwrite: true); } } isTextureSelected = true; texturePathElement.Text = destinationPath.CleanUpPath(); }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("PNG", "*.png"); FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); FileSelection.AddFileTypeFilter("All files", "*.*"); FileSelection.SelectFileTypeFilter("*.png"); FileSelection.Open = true; return true; } }; } break; case 6: { mainElement.RectTransform.NonScaledSize = new Point( mainElement.RectTransform.NonScaledSize.X, mainElement.RectTransform.NonScaledSize.Y * 2); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("ContentPackage")); var rightContainer = new GUIFrame(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), style: null); contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); foreach (ContentPackage contentPackage in ContentPackageManager.EnabledPackages.All) { if (contentPackage != GameMain.VanillaContent) { contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); } } contentPackageDropDown.OnSelected = (obj, userdata) => { ContentPackage = userdata as ContentPackage; updateTexturePath = !isTextureSelected && !IsCopy; UpdatePaths(); return true; }; contentPackageDropDown.Select(0); var contentPackageNameElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 0.5f), rightContainer.RectTransform, Anchor.BottomLeft), GetCharacterEditorTranslation("NewContentPackage").Value) { CaretColor = Color.White, }; var createNewPackageButton = new GUIButton(new RectTransform(new Vector2(0.3f / contentPackageNameElement.RectTransform.RelativeSize.X, 1.0f), contentPackageNameElement.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), TextManager.Get("CreateNew"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => { if (string.IsNullOrEmpty(contentPackageNameElement.Text)) { contentPackageNameElement.Flash(); return false; } if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken")); return false; } string modName = contentPackageNameElement.Text; var modProject = new ModProject { Name = modName }; ContentPackage = ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); contentPackageDropDown.AddItem(ContentPackage.Name, ContentPackage, ContentPackage.Path); contentPackageDropDown.SelectItem(ContentPackage); contentPackageNameElement.Text = ""; return true; }, Enabled = false }; Color textColor = contentPackageNameElement.TextColor; contentPackageNameElement.TextColor *= 0.6f; contentPackageNameElement.OnSelected += (sender, key) => { contentPackageNameElement.Text = ""; }; contentPackageNameElement.OnTextChanged += (textBox, text) => { textBox.TextColor = textColor; createNewPackageButton.Enabled = !string.IsNullOrWhiteSpace(text); return true; }; rightContainer.RectTransform.MinSize = new Point(0, contentPackageDropDown.RectTransform.MinSize.Y + Math.Max(contentPackageNameElement.RectTransform.MinSize.Y, createNewPackageButton.RectTransform.MinSize.Y)); } break; } int contentSize = mainElement.RectTransform.Children.Max(c => c.MinSize.Y) ; mainElement.RectTransform.Resize(new Point(mainElement.Rect.Width, Math.Max(mainElement.Rect.Height, contentSize))); } UpdatePaths(); box.Buttons[0].Parent.RectTransform.SetAsLastChild(); box.Buttons[1].RectTransform.SetAsLastChild(); // Cancel box.Buttons[0].OnClicked += (b, d) => { Wizard.Instance.SelectTab(Tab.None); return true; }; // Next box.Buttons[1].OnClicked += (b, d) => { if (ContentPackage == null) { contentPackageDropDown.Flash(); return false; } string evaluatedTexturePath = ContentPath.FromRaw( contentPackageDropDown.SelectedData as ContentPackage, TexturePath).Value; if (SourceCharacter?.SpeciesName != CharacterPrefab.HumanSpeciesName) { if (!File.Exists(evaluatedTexturePath)) { GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red); texturePathElement.Flash(GUIStyle.Red); return false; } } var path = Path.GetFileName(evaluatedTexturePath); if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { GUI.AddMessage(TextManager.Get("WrongFileType"), GUIStyle.Red); texturePathElement.Flash(GUIStyle.Red); return false; } if (IsCopy) { SourceRagdoll.Texture = evaluatedTexturePath; SourceRagdoll.CanEnterSubmarine = CanEnterSubmarine; SourceRagdoll.CanWalk = CanWalk; SourceRagdoll.Serialize(); Wizard.Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); } else { Wizard.Instance.SelectTab(Tab.Ragdoll); } return true; }; return box; } } private class RagdollView : View { private static RagdollView instance; public static RagdollView Get() => Get(ref instance); public override void Release() => instance = null; protected override GUIMessageBox Create() { var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new LocalizedString[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.65f, 1f)); box.Header.Font = GUIStyle.LargeFont; box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = (int)(20 * GUI.Scale); int elementSize = (int)(40 * GUI.Scale); var frame = new GUIFrame(new RectTransform(new Point(box.Content.Rect.Width - (int)(80 * GUI.xScale), box.Content.Rect.Height - (int)(200 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), style: null, color: ParamsEditor.Color) { CanBeFocused = false }; var content = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), frame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.TopCenter) { Stretch = true, RelativeSpacing = 0.02f }; // HTML GUIMessageBox htmlBox = null; var loadHtmlButton = new GUIButton(new RectTransform(new Point(content.Rect.Width / 3, elementSize), content.RectTransform), GetCharacterEditorTranslation("LoadFromHTML")); // Limbs var limbsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; var limbEditLayout = new GUILayoutGroup(new RectTransform(Vector2.One, limbsElement.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.02f }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("Limbs"), font: GUIStyle.SubHeadingFont); var limbsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) { PlaySoundOnSelect = true, }; var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { OnClicked = (b, d) => { var element = LimbGUIElements.LastOrDefault(); if (element == null) { return false; } element.RectTransform.Parent = null; LimbGUIElements.Remove(element); return true; } }; var addLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton") { OnClicked = (b, d) => { LimbType limbType = LimbType.None; switch (LimbGUIElements.Count) { case 0: limbType = LimbType.Torso; break; case 1: limbType = LimbType.Head; break; } CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: limbType); return true; } }; int _x = 1, _y = 1, w = 100, h = 100; var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), limbEditLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; for (int i = 3; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.RectComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int) { Font = GUIStyle.SmallFont }; switch (i) { case 0: case 1: numberInput.IntValue = 1; numberInput.MinValueInt = 1; numberInput.MaxValueInt = 100; break; case 2: case 3: numberInput.IntValue = 100; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 999; break; } int comp = i; numberInput.OnValueChanged += (numInput) => { switch (comp) { case 0: _x = numInput.IntValue; break; case 1: _y = numInput.IntValue; break; case 2: w = numInput.IntValue; break; case 3: h = numInput.IntValue; break; } }; } inputArea.Recalculate(); new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("AddMultipleLimbsButton")) { OnClicked = (b, d) => { CreateMultipleLimbs(_x, _y); return true; } }; limbsElement.RectTransform.MinSize = new Point(0, limbEditLayout.RectTransform.Children.Max(c => c.MinSize.Y)); // If no elements are defined, create some as default if (LimbGUIElements.None()) { if (IsHumanoid) { CreateMultipleLimbs(2, 6); // Create the missing waist (13th element) CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: LimbType.Waist, sourceRect: new Rectangle(_x, h * LimbGUIElements.Count / 2, w, h)); } else { CreateMultipleLimbs(1, 2); } } void CreateMultipleLimbs(int x, int y) { for (int i = 0; i < x; i++) { for (int j = 0; j < y; j++) { LimbType limbType = LimbType.None; switch (LimbGUIElements.Count) { case 0: limbType = LimbType.Torso; break; case 1: limbType = LimbType.Head; break; } if (IsHumanoid) { switch (LimbGUIElements.Count) { case 2: limbType = LimbType.LeftArm; break; case 3: limbType = LimbType.LeftHand; break; case 4: limbType = LimbType.RightArm; break; case 5: limbType = LimbType.RightHand; break; case 6: limbType = LimbType.LeftThigh; break; case 7: limbType = LimbType.LeftLeg; break; case 8: limbType = LimbType.LeftFoot; break; case 9: limbType = LimbType.RightThigh; break; case 10: limbType = LimbType.RightLeg; break; case 11: limbType = LimbType.RightFoot; break; case 12: limbType = LimbType.Waist; break; } } CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id: LimbGUIElements.Count, limbType: limbType, sourceRect: new Rectangle(i * w, j * h, w, h)); } } } // Joints new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; var jointsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), GetCharacterEditorTranslation("Joints"), font: GUIStyle.SubHeadingFont); var jointButtonElement = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), jointsElement.RectTransform) { RelativeOffset = new Vector2(0.15f, 0) }, style: null) { CanBeFocused = false }; var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)) { PlaySoundOnSelect = true, }; var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), style: "GUIMinusButton") { OnClicked = (b, d) => { var element = JointGUIElements.LastOrDefault(); if (element == null) { return false; } element.RectTransform.Parent = null; JointGUIElements.Remove(element); return true; } }; var addJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height), jointButtonElement.RectTransform) { AbsoluteOffset = new Point(removeJointButton.Rect.Width + 10, 0) }, style: "GUIPlusButton") { OnClicked = (b, d) => { CreateJointGUIElement(jointsList.Content.RectTransform, elementSize); return true; } }; loadHtmlButton.OnClicked = (b, d) => { if (htmlBox == null) { htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new LocalizedString[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); htmlBox.Header.Font = GUIStyle.LargeFont; var element = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); //new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("HTMLPath").Value); LocalizedString title = GetCharacterEditorTranslation("SelectFile"); new GUIButton(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), title) { OnClicked = (button, data) => { FileSelection.OnFileSelected = (file) => { htmlPathElement.Text = file; }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("HTML", "*.html, *.htm"); FileSelection.AddFileTypeFilter("All files", "*.*"); FileSelection.SelectFileTypeFilter("*.html, *.htm"); FileSelection.Open = true; return true; } }; var list = new GUIListBox(new RectTransform(new Vector2(1, 0.8f), htmlBox.Content.RectTransform)); var htmlOutput = new GUITextBlock(new RectTransform(Vector2.One, list.Content.RectTransform), string.Empty) { CanBeFocused = false }; htmlBox.Buttons[0].OnClicked += (_b, _d) => { htmlBox.Close(); return true; }; htmlBox.Buttons[1].OnClicked += (_b, _d) => { LimbGUIElements.ForEach(l => l.RectTransform.Parent = null); LimbGUIElements.Clear(); JointGUIElements.ForEach(j => j.RectTransform.Parent = null); JointGUIElements.Clear(); LimbXElements.Clear(); JointXElements.Clear(); ParseRagdollFromHTML(htmlPathElement.Text, (id, limbName, limbType, rect) => { CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id, limbName, limbType, rect); }, (id1, id2, anchor1, anchor2, jointName) => { CreateJointGUIElement(jointsList.Content.RectTransform, elementSize, id1, id2, anchor1, anchor2, jointName); }); htmlOutput.Text = new XDocument(new XElement("Ragdoll", new object[] { new XAttribute("type", Name), LimbXElements.Values, JointXElements })).ToString(); htmlOutput.CalculateHeightFromText(); list.UpdateScrollBarSize(); return true; }; } else { GUIMessageBox.MessageBoxes.Add(htmlBox); } return true; }; // Previous box.Buttons[0].OnClicked += (b, d) => { Wizard.Instance.SelectTab(Tab.Character); return true; }; // Parse and create box.Buttons[1].OnClicked += (b, d) => { ParseLimbsFromGUIElements(); ParseJointsFromGUIElements(); var main = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("torso", StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("head", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (main == null) { GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), GUIStyle.Red); return false; } if (IsHumanoid) { if (!IsValid(LimbXElements.Values, true, out string missingType)) { GUI.AddMessage(GetCharacterEditorTranslation("MissingLimbType").Replace("[limbtype]", missingType.FormatCamelCaseWithSpaces()), GUIStyle.Red); return false; } } XElement mainLimb = main.Parent; int radius = mainLimb.GetAttributeInt("radius", -1); int height = mainLimb.GetAttributeInt("height", -1); int width = mainLimb.GetAttributeInt("width", -1); int colliderHeight = -1; if (radius == -1) { // the collider is a box -> calculate the capsule if (width == height) { radius = width / 2; colliderHeight = width - radius * 2; } else { if (height > width) { radius = width / 2; colliderHeight = height - radius * 2; } else { radius = height / 2; colliderHeight = width - radius * 2; } } radius = Math.Max(radius, 1); } else if (height > -1 || width > -1) { // the collider is a capsule -> use the capsule as it is colliderHeight = width > height ? width : height; } var colliderAttributes = new List() { new XAttribute("radius", radius) }; if (colliderHeight > -1) { colliderHeight = Math.Max(colliderHeight, 1); if (height > width) { colliderAttributes.Add(new XAttribute("height", colliderHeight)); } else { colliderAttributes.Add(new XAttribute("width", colliderHeight)); } } var colliderElements = new List() { new XElement("collider", colliderAttributes) }; if (IsHumanoid) { // For humanoids, we need a secondary, shorter collider for crouching var secondaryCollider = new XElement("collider", new XAttribute("radius", radius)); if (colliderHeight > -1) { colliderHeight = Math.Max(colliderHeight, 1); if (height > width) { secondaryCollider.Add(new XAttribute("height", colliderHeight * 0.75f)); } else { secondaryCollider.Add(new XAttribute("width", colliderHeight * 0.75f)); } } colliderElements.Add(secondaryCollider); } var mainElement = new XElement("Ragdoll", new XAttribute("type", Name), new XAttribute("texture", TexturePath), new XAttribute("canentersubmarine", CanEnterSubmarine), new XAttribute("canwalk", CanWalk), colliderElements, LimbXElements.Values, JointXElements); Wizard.Instance.CreateCharacter(mainElement); return true; }; return box; } private void CreateLimbGUIElement(RectTransform parent, int elementSize, int id, string name = "", LimbType limbType = LimbType.None, Rectangle? sourceRect = null) { var limbElement = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementSize * 5 + 40), parent), style: null, color: Color.Gray * 0.25f) { CanBeFocused = false }; var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 16 }; var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name, font: GUIStyle.SubHeadingFont); var idField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); var limbTypeField = GUI.CreateEnumField(limbType, elementSize, GetCharacterEditorTranslation("LimbType"), group.RectTransform, font: GUIStyle.Font); var sourceRectField = GUI.CreateRectangleField(sourceRect ?? new Rectangle(0, 100 * LimbGUIElements.Count, 100, 100), elementSize, GetCharacterEditorTranslation("SourceRectangle"), group.RectTransform, font: GUIStyle.Font); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("ID")); new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), idField.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, IntValue = id, OnValueChanged = numInput => { id = numInput.IntValue; string text = nameField.GetChild().Text; string t = string.IsNullOrWhiteSpace(text) ? id.ToString() : text; label.Text = t; } }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), name) { CaretColor = Color.White, }; nameInput.OnTextChanged += (tb, text) => { string t = string.IsNullOrWhiteSpace(text) ? id.ToString() : text; label.Text = t; return true; }; LimbGUIElements.Add(limbElement); } private void CreateJointGUIElement(RectTransform parent, int elementSize, int id1 = 0, int id2 = 1, Vector2? anchor1 = null, Vector2? anchor2 = null, string jointName = "") { var jointElement = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementSize * 6 + 40), parent), style: null, color: Color.Gray * 0.25f) { CanBeFocused = false }; var group = new GUILayoutGroup(new RectTransform(Vector2.One, jointElement.RectTransform)) { AbsoluteSpacing = 2 }; var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), jointName, font: GUIStyle.SubHeadingFont); var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), jointName) { CaretColor = Color.White, }; nameInput.OnTextChanged += (textB, text) => { jointName = text; label.Text = jointName; return true; }; var limb1Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")); var limb1InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb1Field.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, IntValue = id1 }; var limb2Field = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopLeft), GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")); var limb2InputField = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1), limb2Field.RectTransform, Anchor.TopRight), NumberType.Int) { MinValueInt = 0, MaxValueInt = byte.MaxValue, IntValue = id2 }; GUI.CreateVector2Field(anchor1 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1"), group.RectTransform, font: GUIStyle.Font, decimalsToDisplay: 2); GUI.CreateVector2Field(anchor2 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2"), group.RectTransform, font: GUIStyle.Font, decimalsToDisplay: 2); label.Text = GetJointName(jointName); limb1InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); limb2InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); JointGUIElements.Add(jointElement); string GetJointName(string n) => string.IsNullOrWhiteSpace(n) ? $"{GetCharacterEditorTranslation("Joint")} {limb1InputField.IntValue} - {limb2InputField.IntValue}" : n; } } private abstract class View { // Easy accessors to the common data. public bool IsCopy => Instance.IsCopy; public IEnumerable SourceAnimations => Instance.SourceAnimations; public CharacterParams SourceCharacter => Instance.SourceCharacter; public RagdollParams SourceRagdoll => Instance.SourceRagdoll; public Identifier Name { get => Instance.name; set => Instance.name = value; } public bool IsHumanoid { get => Instance.isHumanoid; set => Instance.isHumanoid = value; } public bool CanEnterSubmarine { get => Instance.canEnterSubmarine; set => Instance.canEnterSubmarine = value; } public bool CanWalk { get => Instance.canWalk; set => Instance.canWalk = value; } public ContentPackage ContentPackage { get => Instance.contentPackage; set => Instance.contentPackage = value; } public string TexturePath { get => Instance.texturePath; set => Instance.texturePath = value; } public string XMLPath { get => Instance.xmlPath; set => Instance.xmlPath = value; } public Dictionary LimbXElements { get => Instance.limbXElements; set => Instance.limbXElements = value; } public List LimbGUIElements { get => Instance.limbGUIElements; set => Instance.limbGUIElements = value; } public List JointXElements { get => Instance.jointXElements; set => Instance.jointXElements = value; } public List JointGUIElements { get => Instance.jointGUIElements; set => Instance.jointGUIElements = value; } private GUIMessageBox box; public GUIMessageBox Box { get { if (box == null) { box = Create(); } return box; } } protected abstract GUIMessageBox Create(); protected static T Get(ref T instance) where T : View, new() { if (instance == null) { instance = new T(); } return instance; } public abstract void Release(); protected void ParseLimbsFromGUIElements() { LimbXElements.Clear(); for (int i = 0; i < LimbGUIElements.Count; i++) { var limbGUIElement = LimbGUIElements[i]; var allChildren = limbGUIElement.GetAllChildren(); GUITextBlock GetField(LocalizedString n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; int id = GetField(GetCharacterEditorTranslation("ID")).Parent.GetChild().IntValue; string limbName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; LimbType limbType = (LimbType)GetField(GetCharacterEditorTranslation("LimbType")).Parent.GetChild().SelectedData; // Reverse, because the elements are created from right to left var rectInputs = GetField(GetCharacterEditorTranslation("SourceRectangle")).Parent.GetAllChildren().Reverse().ToArray(); int width = rectInputs[2].IntValue; int height = rectInputs[3].IntValue; var colliderAttributes = new List(); // Capsules/Circles //if (width == height) //{ // colliderAttributes.Add(new XAttribute("radius", (int)(width / 2 * 0.85f))); //} //else //{ // if (height > width) // { // colliderAttributes.Add(new XAttribute("radius", (int)(width / 2 * 0.85f))); // colliderAttributes.Add(new XAttribute("height",(int) (height - width * 0.85f))); // } // else // { // colliderAttributes.Add(new XAttribute("radius", (int)(height / 2 * 0.85f))); // colliderAttributes.Add(new XAttribute("width", (int)(width - height * 0.85f))); // } //} // Rectangles colliderAttributes.Add(new XAttribute("height", (int)(height * 0.85f))); colliderAttributes.Add(new XAttribute("width", (int)(width * 0.85f))); idToCodeName.TryGetValue(id, out string notes); LimbXElements.Add(id.ToString(), new XElement("limb", new XAttribute("id", id), new XAttribute("name", limbName), new XAttribute("type", limbType.ToString()), colliderAttributes, new XElement("sprite", new XAttribute("texture", ""), new XAttribute("sourcerect", $"{rectInputs[0].IntValue}, {rectInputs[1].IntValue}, {width}, {height}")), new XAttribute("notes", null ?? string.Empty) )); } } protected void ParseJointsFromGUIElements() { JointXElements.Clear(); for (int i = 0; i < JointGUIElements.Count; i++) { var jointGUIElement = JointGUIElements[i]; var allChildren = jointGUIElement.GetAllChildren(); GUITextBlock GetField(LocalizedString n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; string jointName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; int limb1ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")).Parent.GetChild().IntValue; int limb2ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")).Parent.GetChild().IntValue; // Reverse, because the elements are created from right to left var anchor1Inputs = GetField(GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1")).Parent.GetAllChildren().Reverse().ToArray(); var anchor2Inputs = GetField(GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2")).Parent.GetAllChildren().Reverse().ToArray(); JointXElements.Add(new XElement("joint", new XAttribute("name", jointName), new XAttribute("limb1", limb1ID), new XAttribute("limb2", limb2ID), new XAttribute("limb1anchor", $"{anchor1Inputs[0].FloatValue.Format(2)}, {anchor1Inputs[1].FloatValue.Format(2)}"), new XAttribute("limb2anchor", $"{anchor2Inputs[0].FloatValue.Format(2)}, {anchor2Inputs[1].FloatValue.Format(2)}"))); } } Dictionary idToCodeName = new Dictionary(); protected void ParseRagdollFromHTML(string path, Action limbCallback = null, Action jointCallback = null) { // TODO: parse as xml files -> allows to load ragdolls onto the wizard. //XDocument doc = XMLExtensions.TryLoadXml(path); //var xElements = doc.Elements().ToArray(); string html = string.Empty; try { html = File.ReadAllText(path); } catch (Exception e) { DebugConsole.ThrowError(GetCharacterEditorTranslation("FailedToReadHTML").Replace("[path]", path), e); return; } var lines = html.Split(new string[] { "", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) .Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height")); int id = 0; Dictionary hierarchyToID = new Dictionary(); Dictionary idToHierarchy = new Dictionary(); Dictionary idToPositionCode = new Dictionary(); Dictionary idToName = new Dictionary(); idToCodeName.Clear(); foreach (var line in lines) { var codeNames = new string(line.SkipWhile(c => c != '>').Skip(1).ToArray()).Split(','); for (int i = 0; i < codeNames.Length; i++) { string codeName = codeNames[i].Trim(); if (string.IsNullOrWhiteSpace(codeName)) { continue; } idToCodeName.Add(id, codeName); string limbName = new string(codeName.SkipWhile(c => c != '_').Skip(1).ToArray()); if (string.IsNullOrWhiteSpace(limbName)) { continue; } idToName.Add(id, limbName); var parts = line.Split(' '); int ParseToInt(string selector) { string part = parts.First(p => p.Contains(selector)); string s = new string(part.SkipWhile(c => c != ':').Skip(1).TakeWhile(c => char.IsNumber(c)).ToArray()); int.TryParse(s, out int v); return v; }; // example: 111311cr -> 111311 string hierarchy = new string(codeName.TakeWhile(c => char.IsNumber(c)).ToArray()); if (hierarchyToID.ContainsKey(hierarchy)) { DebugConsole.ThrowError(GetCharacterEditorTranslation("MultipleItemsWithSameHierarchy").Replace("[hierarchy]", hierarchy).Replace("[name]", codeName)); return; } hierarchyToID.Add(hierarchy, id); idToHierarchy.Add(id, hierarchy); string positionCode = new string(codeName.SkipWhile(c => char.IsNumber(c)).TakeWhile(c => c != '_').ToArray()); idToPositionCode.Add(id, positionCode.ToLowerInvariant()); int x = ParseToInt("left"); int y = ParseToInt("top"); int width = ParseToInt("width"); int height = ParseToInt("height"); // This is overridden when the data is loaded from the gui fields. LimbXElements.Add(hierarchy, new XElement("limb", new XAttribute("id", id), new XAttribute("name", limbName), new XAttribute("type", ParseLimbType(limbName).ToString()), new XElement("sprite", new XAttribute("texture", ""), new XAttribute("sourcerect", $"{x}, {y}, {width}, {height}")) )); limbCallback?.Invoke(id, limbName, ParseLimbType(limbName), new Rectangle(x, y, width, height)); id++; } } for (int i = 0; i < id; i++) { if (idToHierarchy.TryGetValue(i, out string hierarchy)) { if (hierarchy != "0") { // NEW LOGIC: if hierarchy length == 1, parent to 0 // Else parent to the last bone in the current hierarchy (11 is parented to 1, 212 is parented to 21 etc) string parent = hierarchy.Length > 1 ? hierarchy.Remove(hierarchy.Length - 1, 1) : "0"; if (hierarchyToID.TryGetValue(parent, out int parentID)) { Vector2 anchor1 = Vector2.Zero; Vector2 anchor2 = Vector2.Zero; idToName.TryGetValue(parentID, out string parentName); idToName.TryGetValue(i, out string limbName); string jointName = $"{GetCharacterEditorTranslation("Joint")} {parentName} - {limbName}"; if (idToPositionCode.TryGetValue(i, out string positionCode)) { float scalar = 0.8f; if (LimbXElements.TryGetValue(parent, out XElement parentElement)) { Rectangle parentSourceRect = parentElement.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); float parentWidth = parentSourceRect.Width / 2 * scalar; float parentHeight = parentSourceRect.Height / 2 * scalar; switch (positionCode) { case "tl": // -1, 1 anchor1 = new Vector2(-parentWidth, parentHeight); break; case "tc": // 0, 1 anchor1 = new Vector2(0, parentHeight); break; case "tr": // -1, 1 anchor1 = new Vector2(-parentWidth, parentHeight); break; case "cl": // -1, 0 anchor1 = new Vector2(-parentWidth, 0); break; case "cr": // 1, 0 anchor1 = new Vector2(parentWidth, 0); break; case "bl": // -1, -1 anchor1 = new Vector2(-parentWidth, -parentHeight); break; case "bc": // 0, -1 anchor1 = new Vector2(0, -parentHeight); break; case "br": // 1, -1 anchor1 = new Vector2(parentWidth, -parentHeight); break; } if (LimbXElements.TryGetValue(hierarchy, out XElement element)) { Rectangle sourceRect = element.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); float width = sourceRect.Width / 2 * scalar; float height = sourceRect.Height / 2 * scalar; switch (positionCode) { // Inverse case "tl": // br anchor2 = new Vector2(-width, -height); break; case "tc": // bc anchor2 = new Vector2(0, -height); break; case "tr": // bl anchor2 = new Vector2(-width, -height); break; case "cl": // cr anchor2 = new Vector2(width, 0); break; case "cr": // cl anchor2 = new Vector2(-width, 0); break; case "bl": // tr anchor2 = new Vector2(-width, height); break; case "bc": // tc anchor2 = new Vector2(0, height); break; case "br": // tl anchor2 = new Vector2(-width, height); break; } } } } // This is overridden when the data is loaded from the gui fields. JointXElements.Add(new XElement("joint", new XAttribute("name", jointName), new XAttribute("limb1", parentID), new XAttribute("limb2", i), new XAttribute("limb1anchor", $"{anchor1.X.Format(2)}, {anchor1.Y.Format(2)}"), new XAttribute("limb2anchor", $"{anchor2.X.Format(2)}, {anchor2.Y.Format(2)}") )); jointCallback?.Invoke(parentID, i, anchor1, anchor2, jointName); } } } } } protected LimbType ParseLimbType(string limbName) { var limbType = LimbType.None; string n = limbName.ToLowerInvariant(); switch (n) { case "head": limbType = LimbType.Head; break; case "torso": limbType = LimbType.Torso; break; case "waist": case "pelvis": limbType = LimbType.Waist; break; case "tail": limbType = LimbType.Tail; break; } if (limbType == LimbType.None) { if (n.Contains("tail")) { limbType = LimbType.Tail; } else if (n.Contains("arm") && !n.Contains("lower")) { if (n.Contains("right")) { limbType = LimbType.RightArm; } else if (n.Contains("left")) { limbType = LimbType.LeftArm; } } else if (n.Contains("hand") || n.Contains("palm")) { if (n.Contains("right")) { limbType = LimbType.RightHand; } else if (n.Contains("left")) { limbType = LimbType.LeftHand; } } else if (n.Contains("thigh") || n.Contains("upperleg")) { if (n.Contains("right")) { limbType = LimbType.RightThigh; } else if (n.Contains("left")) { limbType = LimbType.LeftThigh; } } else if (n.Contains("shin") || n.Contains("lowerleg")) { if (n.Contains("right")) { limbType = LimbType.RightLeg; } else if (n.Contains("left")) { limbType = LimbType.LeftLeg; } } else if (n.Contains("foot")) { if (n.Contains("right")) { limbType = LimbType.RightFoot; } else if (n.Contains("left")) { limbType = LimbType.LeftFoot; } } } return limbType; } public static bool IsValid(IEnumerable elements, bool isHumanoid, out string missingType) { missingType = "none"; if (!HasAtLeastOneLimbOfType(elements, "torso") && !HasAtLeastOneLimbOfType(elements, "head")) { missingType = "TorsoOrHead"; return false; } if (isHumanoid) { if (!HasOnlyOneLimbOfType(elements, missingType = "LeftArm")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "LeftHand")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "RightArm")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "RightHand")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "Waist")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "LeftThigh")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "LeftLeg")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "LeftFoot")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "RightThigh")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "RightLeg")) { return false; } if (!HasOnlyOneLimbOfType(elements, missingType = "RightFoot")) { return false; } } return true; } public static bool HasAtLeastOneLimbOfType(IEnumerable elements, string type) => elements.Any(e => IsType(e, type)); public static bool HasOnlyOneLimbOfType(IEnumerable elements, string type) => elements.Count(e => IsType(e, type)) == 1; private static bool IsType(XElement element, string type) => element.GetAttributeString("type", "").Equals(type, StringComparison.OrdinalIgnoreCase); } } }