using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Linq; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Items.Components; using System.Collections.Immutable; namespace Barotrauma { partial class CharacterInfo { private static Sprite infoAreaPortraitBG; public bool LastControlled; public int CrewListIndex { get; set; } = int.MaxValue; //default to the bottom of the list private Sprite disguisedPortrait; private List disguisedAttachmentSprites; private Vector2? disguisedSheetIndex; private Sprite disguisedJobIcon; private Color disguisedJobColor; private Color disguisedHairColor; private Color disguisedFacialHairColor; private Color disguisedSkinColor; private Sprite tintMask; private float tintHighlightThreshold; private float tintHighlightMultiplier; public bool ShowTalentResetPopupOnOpen = true; public static void Init() { infoAreaPortraitBG = GUIStyle.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement) { ContentXElement maskElement = limbElement.GetChildElement("tintmask"); if (maskElement != null) { ContentPath tintMaskPath = maskElement.GetAttributeContentPath("texture"); if (!tintMaskPath.IsNullOrEmpty()) { VerifySpriteTagsLoaded(); tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); tintHighlightMultiplier = maskElement.GetAttributeFloat("highlightmultiplier", 0.8f); } } } public GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon = null) { var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { RelativeSpacing = 0.05f //Stretch = true }; var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform), isHorizontal: true); new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font; var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { RelativeSpacing = 0.02f, Stretch = true }; Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) { ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero }; if (permissionIcon != null) { Point iconSize = permissionIcon.SourceRect.Size; int iconWidth = (int)((float)characterNameBlock.Rect.Height / iconSize.Y * iconSize.X); new GUIImage(new RectTransform(new Point(iconWidth, characterNameBlock.Rect.Height), characterNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; } if (Job != null) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) { Padding = Vector4.Zero }; } if (PersonalityTrait != null) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), PersonalityTrait.DisplayName), font: font) { Padding = Vector4.Zero }; } GUIButton manageTalentButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), text: TextManager.Get("ClientPermission.ManageBotTalents"), style: "GUIButtonSmall") { Enabled = false, UserData = TalentMenu.ManageBotTalentsButtonUserData, TextBlock = { AutoScaleHorizontal = true } }; if (TalentMenu.CanManageTalents(this)) { manageTalentButton.Enabled = true; } if (Job != null && Character is not { IsDead: true }) { var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { Stretch = true }; var skills = Job.GetSkills().ToList(); skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), skill.DisplayName, textColor: textColor, font: font) { Padding = Vector4.Zero }; float modifiedSkillLevel = skill.Level; if (Character != null) { modifiedSkillLevel = Character.GetSkillLevel(skill.Identifier); } if (!MathUtils.NearlyEqual(MathF.Round(modifiedSkillLevel), MathF.Round(skill.Level))) { int skillChange = (int)MathF.Round(modifiedSkillLevel - skill.Level); string changeText = $"{(skillChange > 0 ? "+" : "") + skillChange}"; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), $"{(int)skill.Level} ({changeText})", textColor: textColor, font: font, textAlignment: Alignment.CenterRight); } else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); } } } else if (Character is { IsDead: true }) { var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { Stretch = true }; LocalizedString deadDescription = TextManager.Get("deceased") + "\n" + (Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.Get("CauseOfDeath." + Character.CauseOfDeath.Type.ToString())); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUIStyle.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; } if (returnParent) { return frame; } else { return paddedFrame; } } private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) { if (HeadSprite == null) { return; } Vector2 targetAreaSize = componentRect.Size.ToVector2(); float scale = Math.Min(targetAreaSize.X / _headSprite.size.X, targetAreaSize.Y / _headSprite.size.Y); DrawIcon(sb, componentRect.Location.ToVector2() + _headSprite.size / 2 * scale, targetAreaSize); } public GUIFrame CreateCharacterFrame(GUIComponent parent, string text, object userData) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, 40), parent.RectTransform) { IsFixedSize = false }, "ListBoxElement") { UserData = userData }; Color? textColor = null; if (Job != null) { textColor = Job.Prefab.UIColor; } GUITextBlock textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(40, 0) }, text, textColor: textColor, font: GUIStyle.SmallFont); new GUICustomComponent(new RectTransform(new Point(frame.Rect.Height, frame.Rect.Height), frame.RectTransform, Anchor.CenterLeft) { IsFixedSize = false }, onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); return frame; } partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification) { if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } // if we increased by more than 1 in one increase, then display special color (for talents) bool specialIncrease = Math.Abs(newLevel - prevLevel) >= 1.0f; if ((int)newLevel > (int)prevLevel) { Character.Controlled?.SelectedItem?.OnPlayerSkillsChanged(); int increase = Math.Max((int)newLevel - (int)prevLevel, 1); Character?.AddMessage( "+[value] "+ TextManager.Get("SkillName." + skillIdentifier).Value, specialIncrease ? GUIStyle.Orange : GUIStyle.Green, playSound: Character == Character.Controlled, skillIdentifier, increase); } else if (forceNotification) { float change = newLevel - prevLevel; if (Math.Abs(change) > 0.01f) { string sign = change > 0 ? "+" : "-"; Character?.AddMessage( $"{sign}{Math.Round(change, 2)} {TextManager.Get("SkillName." + skillIdentifier).Value}", specialIncrease ? GUIStyle.Orange : GUIStyle.Green, playSound: Character == Character.Controlled); } } } partial void OnExperienceChanged(int prevAmount, int newAmount) { if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } GameSession.TabMenuInstance?.OnExperienceChanged(Character); if (newAmount > prevAmount) { int increase = newAmount - prevAmount; Character?.AddMessage( "+[value] " + TextManager.Get("experienceshort").Value, GUIStyle.Blue, playSound: Character == Character.Controlled, "exp".ToIdentifier(), increase); } } private void GetDisguisedSprites(IdCard idCard) { if (idCard.Item.Tags == string.Empty) return; if (idCard.StoredOwnerAppearance.JobPrefab == null || idCard.StoredOwnerAppearance.Portrait == null) { var readTags = idCard.Item.Tags.Split(',') .Where(s => s.Contains(':')) .Select(s => s.Split(':')) .Select(s => (s[0].ToIdentifier(),s[1])) .ToImmutableDictionary(); if (readTags.None()) { return; } if (idCard.StoredOwnerAppearance.JobPrefab == null) { idCard.StoredOwnerAppearance.ExtractJobPrefab(readTags); } if (idCard.StoredOwnerAppearance.Portrait == null) { idCard.StoredOwnerAppearance.ExtractAppearance(this, idCard); } } if (idCard.StoredOwnerAppearance.JobPrefab != null) { disguisedJobIcon = idCard.StoredOwnerAppearance.JobPrefab.Icon; disguisedJobColor = idCard.StoredOwnerAppearance.JobPrefab.UIColor; } disguisedPortrait = idCard.StoredOwnerAppearance.Portrait; disguisedSheetIndex = idCard.StoredOwnerAppearance.SheetIndex; disguisedAttachmentSprites = idCard.StoredOwnerAppearance.Attachments; disguisedHairColor = idCard.StoredOwnerAppearance.HairColor; disguisedFacialHairColor = idCard.StoredOwnerAppearance.FacialHairColor; disguisedSkinColor = idCard.StoredOwnerAppearance.SkinColor; } partial void LoadAttachmentSprites() { if (attachmentSprites == null) { attachmentSprites = new List(); } if (!IsAttachmentsLoaded) { LoadHeadAttachments(); } Head.FaceAttachment?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.FaceAttachment))); Head.BeardElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); Head.MoustacheElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); Head.HairElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); } // Doesn't work if the head's source rect does not start at 0,0. public static Point CalculateOffset(Sprite sprite, Point offset) => sprite.SourceRect.Size * offset; public void CalculateHeadPosition(Sprite sprite) { if (sprite == null) { return; } if (Head.SheetIndex == null) { return; } Point location = CalculateOffset(sprite, Head.SheetIndex.ToPoint()); sprite.SourceRect = new Rectangle(location, sprite.SourceRect.Size); } public void DrawBackground(SpriteBatch spriteBatch) { if (infoAreaPortraitBG == null) { return; } infoAreaPortraitBG.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea.Location.ToVector2(), Color.White, Vector2.Zero, 0.0f, scale: new Vector2( HUDLayoutSettings.BottomRightInfoArea.Width / (float)infoAreaPortraitBG.SourceRect.Width, HUDLayoutSettings.BottomRightInfoArea.Height / (float)infoAreaPortraitBG.SourceRect.Height)); } public void DrawForeground(SpriteBatch spriteBatch) { if (Character is null || !(GameMain.GameSession?.Campaign is MultiPlayerCampaign)) { return; } const int million = 1000000; int xfraction = (int)(HUDLayoutSettings.BottomRightInfoArea.Width * 0.2f); int yoffset = GUI.IntScale(6); int walletAmount = Character.Wallet.Balance; LocalizedString str = walletAmount >= million ? TextManager.Get("crewwallet.balance.toomuchtoshow") : TextManager.FormatCurrency(walletAmount); Vector2 size = GUIStyle.Font.MeasureString(str); int barHeight = GUI.IntScale(18); Rectangle barRect = new Rectangle((int)(HUDLayoutSettings.BottomRightInfoArea.X + xfraction / 2.5f), HUDLayoutSettings.BottomRightInfoArea.Bottom - barHeight - yoffset, HUDLayoutSettings.BottomRightInfoArea.Width - xfraction, barHeight); float textScale = Math.Max(0.1f, Math.Min(barRect.Width / size.X, barRect.Height / size.Y)) - 0.01f; GUIStyle.WalletPortraitBG.Draw(spriteBatch, barRect, Color.White); int iconSize = GUI.IntScale(28); int iconXOffset = iconSize / 2; Rectangle iconRect = new Rectangle(barRect.Right - iconXOffset, barRect.Top - iconSize / 4, iconSize, iconSize); GUIStyle.CrewWalletIconSmall.Draw(spriteBatch, iconRect, Color.White); var (scaledTextSizeX, scaledTextSizeY) = size * textScale; GUIStyle.Font.DrawString(spriteBatch, str, new Vector2(barRect.Right - iconXOffset - scaledTextSizeX - GUI.IntScale(4), barRect.Center.Y - scaledTextSizeY / 2), GUIStyle.TextColorNormal, 0f, Vector2.Zero, textScale, SpriteEffects.None, 0f); } //TODO: I hate this so much :( private SpriteBatch.EffectWithParams headEffectParameters; private Dictionary attachmentEffectParameters = new Dictionary(); private void SetHeadEffect(SpriteBatch spriteBatch) { headEffectParameters.Effect ??= GameMain.GameScreen.ThresholdTintEffect; headEffectParameters.Params ??= new Dictionary(); headEffectParameters.Params["xBaseTexture"] = HeadSprite.Texture; headEffectParameters.Params["xTintMaskTexture"] = tintMask?.Texture ?? GUI.WhiteTexture; headEffectParameters.Params["xCutoffTexture"] = GUI.WhiteTexture; headEffectParameters.Params["baseToCutoffSizeRatio"] = 1.0f; headEffectParameters.Params["highlightThreshold"] = tintHighlightThreshold; headEffectParameters.Params["highlightMultiplier"] = tintHighlightMultiplier; spriteBatch.SwapEffect(headEffectParameters); } private void SetAttachmentEffect(SpriteBatch spriteBatch, WearableSprite attachment) { if (!attachmentEffectParameters.ContainsKey(attachment.Type)) { attachmentEffectParameters.Add(attachment.Type, new SpriteBatch.EffectWithParams(GameMain.GameScreen.ThresholdTintEffect, new Dictionary())); } var parameters = attachmentEffectParameters[attachment.Type].Params; parameters["xBaseTexture"] = attachment.Sprite.Texture; parameters["xTintMaskTexture"] = GUI.WhiteTexture; parameters["xCutoffTexture"] = GUI.WhiteTexture; parameters["baseToCutoffSizeRatio"] = 1.0f; parameters["highlightThreshold"] = tintHighlightThreshold; parameters["highlightMultiplier"] = tintHighlightMultiplier; spriteBatch.SwapEffect(attachmentEffectParameters[attachment.Type]); } private Color GetAttachmentColor(WearableSprite attachment, Color hairColor, Color facialHairColor) { switch (attachment.Type) { case WearableType.Hair: return hairColor; case WearableType.Beard: case WearableType.Moustache: return facialHairColor; default: return Color.White; } } public void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize, bool flip = false) { var headSprite = HeadSprite; if (headSprite != null) { var spriteEffects = flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None; var currEffect = spriteBatch.GetCurrentEffect(); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); SetHeadEffect(spriteBatch); headSprite.Draw(spriteBatch, screenPos, scale: scale, color: Head.SkinColor, spriteEffect: spriteEffects); if (AttachmentSprites != null) { float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { SetAttachmentEffect(spriteBatch, attachment); DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, Head.HairColor, Head.FacialHairColor), spriteEffects: spriteEffects); depthStep += depthStep; } } spriteBatch.SwapEffect(currEffect); } } public void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) return; var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; if (icon == null) { return; } Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; icon.Draw(spriteBatch, area.Center.ToVector2(), iconColor, scale: Math.Min(area.Width / (float)icon.SourceRect.Width, area.Height / (float)icon.SourceRect.Height)); } private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, Color? color = null, SpriteEffects spriteEffects = SpriteEffects.None) { if (attachment.InheritSourceRect) { if (attachment.SheetIndex.HasValue) { attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, attachment.SheetIndex.Value), head.SourceRect.Size); } else if (sheetIndex.HasValue) { attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, sheetIndex.Value.ToPoint()), head.SourceRect.Size); } else { attachment.Sprite.SourceRect = head.SourceRect; } } Vector2 origin; if (attachment.InheritOrigin) { origin = head.Origin; attachment.Sprite.Origin = origin; } else { origin = attachment.Sprite.Origin; if (spriteEffects.HasFlag(SpriteEffects.FlipHorizontally)) { origin.X = attachment.Sprite.size.X - origin.X; } if (spriteEffects.HasFlag(SpriteEffects.FlipVertically)) { origin.Y = attachment.Sprite.size.Y - origin.Y; } } float depth = attachment.Sprite.Depth; if (attachment.InheritLimbDepth) { depth = head.Depth - depthStep; } attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound = true) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); string originalName = inc.ReadString(); bool renamingEnabled = inc.ReadBoolean(); BotStatus botStatus = (BotStatus)inc.ReadByte(); int salary = inc.ReadInt32(); int tagCount = inc.ReadByte(); HashSet tagSet = new HashSet(); for (int i = 0; i < tagCount; i++) { tagSet.Add(inc.ReadIdentifier()); } int hairIndex = inc.ReadByte(); int beardIndex = inc.ReadByte(); int moustacheIndex = inc.ReadByte(); int faceAttachmentIndex = inc.ReadByte(); Color skinColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8(); Identifier npcId = inc.ReadIdentifier(); Identifier factionId = inc.ReadIdentifier(); float minReputationToHire = 0.0f; if (!factionId.IsEmpty) { minReputationToHire = inc.ReadSingle(); } uint jobIdentifier = inc.ReadUInt32(); int variant = inc.ReadByte(); JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (jobIdentifier > 0) { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); if (jobPrefab == null && requireJobPrefabFound) { throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); } byte skillCount = inc.ReadByte(); for (int i = 0; i < skillCount; i++) { Identifier skillIdentifier = inc.ReadIdentifier(); float skillLevel = inc.ReadSingle(); skillLevels.Add(skillIdentifier, skillLevel); } } CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, variant, npcIdentifier: npcId) { ID = infoID, MinReputationToHire = (factionId, minReputationToHire), RenamingEnabled = renamingEnabled }; ch.BotStatus = botStatus; ch.Salary = salary; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; ch.Head.HairColor = hairColor; ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); ch.Job?.OverrideSkills(skillLevels); ch.ExperiencePoints = inc.ReadInt32(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); ch.PermanentlyDead = inc.ReadBoolean(); ch.TalentRefundPoints = inc.ReadInt32(); ch.TalentResetCount = inc.ReadInt32(); return ch; } public void CreateIcon(RectTransform rectT) { LoadHeadAttachments(); new GUICustomComponent(rectT, onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); } public class AppearanceCustomizationMenu : IDisposable { public readonly CharacterInfo CharacterInfo; public GUIListBox HeadSelectionList = null; public bool HasIcon = true; public GUIScrollBar.OnMovedHandler OnSliderMoved = null; public GUIScrollBar.OnMovedHandler OnSliderReleased = null; public Action OnHeadSwitch = null; private readonly GUIComponent parentComponent; private readonly List characterSprites = new List(); public GUIButton RandomizeButton; public AppearanceCustomizationMenu(CharacterInfo info, GUIComponent parent, bool hasIcon = true) { CharacterInfo = info; parentComponent = parent; HasIcon = hasIcon; RecreateFrameContents(); } public void RecreateFrameContents() { var info = CharacterInfo; HeadSelectionList = null; parentComponent.ClearChildren(); ClearSprites(); float contentWidth = HasIcon ? 0.75f : 1.0f; var listBox = new GUIListBox( new RectTransform(new Vector2(contentWidth, 1.0f), parentComponent.RectTransform, Anchor.CenterLeft)) { CanBeFocused = false, CanTakeKeyBoardFocus = false }; var content = listBox.Content; info.LoadHeadAttachments(); if (HasIcon) { info.CreateIcon( new RectTransform(new Vector2(0.25f, 1.0f), parentComponent.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(-0.01f, 0.0f) }); } RectTransform createItemRectTransform(Identifier labelTag, float width = 0.6f) { var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.166f), content.RectTransform)); var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), TextManager.Get(labelTag), font: GUIStyle.SubHeadingFont); var bottomItem = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), style: null); return new RectTransform(new Vector2(width, 1.0f), bottomItem.RectTransform, Anchor.Center); } RectTransform menuCategoryRT = createItemRectTransform(info.Prefab.MenuCategoryVar, 1.0f); GUILayoutGroup menuCategoryContainer = new GUILayoutGroup(menuCategoryRT, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; void createMenuCategoryButton(Identifier tag) { new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), menuCategoryContainer.RectTransform), TextManager.Get(tag), style: "ListBoxElement") { UserData = tag, OnClicked = OpenHeadSelection, Selected = info.Head.Preset.TagSet.Contains(tag) }; } foreach (var tag in info.Prefab.VarTags[info.Prefab.MenuCategoryVar].OrderBy(t => t.Value).Reverse()) { createMenuCategoryButton(tag); } List attachmentSliders = new List(); void createAttachmentSlider(int initialValue, WearableType wearableType) { int attachmentCount = info.CountValidAttachmentsOfType(wearableType); if (attachmentCount > 0) { var labelTag = wearableType == WearableType.FaceAttachment ? "FaceAttachment.Accessories".ToIdentifier() : $"FaceAttachment.{wearableType}".ToIdentifier(); var sliderItemRT = createItemRectTransform(labelTag); var slider = new GUIScrollBar(sliderItemRT, style: "GUISlider") { Range = new Vector2(0, attachmentCount), StepValue = 1, OnMoved = (bar, scroll) => SwitchAttachment(bar, wearableType), OnReleased = OnSliderReleased, BarSize = 1.0f / (float)(attachmentCount + 1) }; slider.BarScrollValue = initialValue; attachmentSliders.Add(slider); } } createAttachmentSlider(info.Head.HairIndex, WearableType.Hair); createAttachmentSlider(info.Head.BeardIndex, WearableType.Beard); createAttachmentSlider(info.Head.MoustacheIndex, WearableType.Moustache); createAttachmentSlider(info.Head.FaceAttachmentIndex, WearableType.FaceAttachment); void createColorSelector(Identifier labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, Action setter) { var selectorItemRT = createItemRectTransform(labelTag, 0.4f); var dropdown = new GUIDropDown(selectorItemRT) { AllowNonText = true }; var listBoxSize = dropdown.ListBox.RectTransform.RelativeSize; dropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.75f, listBoxSize.Y); var dropdownButton = dropdown.GetChild(); var buttonFrame = new GUIFrame( new RectTransform(Vector2.One * 0.7f, dropdownButton.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, style: null); Color? previewingColor = null; dropdown.OnSelected = (component, color) => { previewingColor = null; setter((Color)color); buttonFrame.Color = getter(); buttonFrame.HoverColor = getter(); return true; }; buttonFrame.Color = getter(); buttonFrame.HoverColor = getter(); dropdown.ListBox.UseGridLayout = true; foreach (var option in options) { var optionElement = new GUIFrame( new RectTransform(new Vector2(0.25f, 1.0f / 3.0f), dropdown.ListBox.Content.RectTransform), style: "ListBoxElement") { UserData = option.Color, CanBeFocused = true }; var colorElement = new GUIFrame( new RectTransform(Vector2.One * 0.75f, optionElement.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: null) { Color = option.Color, HoverColor = option.Color, OutlineColor = Color.Lerp(Color.Black, option.Color, 0.5f), CanBeFocused = false }; } var childToSelect = dropdown.ListBox.Content.FindChild(c => (Color)c.UserData == getter()); dropdown.Select(dropdown.ListBox.Content.GetChildIndex(childToSelect)); //The following exists to track mouseover to preview colors before selecting them new GUICustomComponent(new RectTransform(Vector2.One, buttonFrame.RectTransform), onUpdate: (deltaTime, component) => { if (GUI.MouseOn is GUIFrame { Parent: { } p } hoveredFrame && dropdown.ListBox.Content.IsParentOf(hoveredFrame)) { previewingColor ??= getter(); Color color = (Color)(dropdown.ListBox.Content.FindChild(c => c == hoveredFrame || c.IsParentOf(hoveredFrame))?.UserData ?? dropdown.SelectedData ?? getter()); setter(color); buttonFrame.Color = getter(); buttonFrame.HoverColor = getter(); } else if (previewingColor.HasValue) { setter(previewingColor.Value); buttonFrame.Color = getter(); buttonFrame.HoverColor = getter(); previewingColor = null; } }, onDraw: null) { CanBeFocused = false, Visible = true }; } if (info.CountValidAttachmentsOfType(WearableType.Hair) > 0) { createColorSelector($"Customization.{nameof(info.Head.HairColor)}".ToIdentifier(), info.HairColors, () => info.Head.HairColor, (color) => info.Head.HairColor = color); } if (info.CountValidAttachmentsOfType(WearableType.Moustache) > 0 || info.CountValidAttachmentsOfType(WearableType.Beard) > 0) { createColorSelector($"Customization.{nameof(info.Head.FacialHairColor)}".ToIdentifier(), info.FacialHairColors, () => info.Head.FacialHairColor, (color) => info.Head.FacialHairColor = color); } createColorSelector($"Customization.{nameof(info.Head.SkinColor)}".ToIdentifier(), info.SkinColors, () => info.Head.SkinColor, (color) => info.Head.SkinColor = color); #if DEBUG new GUIButton(new RectTransform(Vector2.One * 0.12f, parentComponent.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) { RelativeOffset = new Vector2(0.01f, 0.005f) }, style: "SaveButton", color: Color.Magenta) { ToolTip = "DEBUG ONLY: copy the character info XML to clipboard", OnClicked = (button, o) => { XElement element = info.Save(null); Clipboard.SetText(element.ToString()); return false; } }; #endif RandomizeButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, parentComponent.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) { RelativeOffset = new Vector2(0.01f, 0.005f) }, style: "RandomizeButton") { OnClicked = (button, o) => { var headPreset = info.Prefab.Heads.GetRandom(Rand.RandSync.Unsynced); info.Head = new HeadInfo(info, headPreset); info.SetAttachments(Rand.RandSync.Unsynced); info.SetColors(Rand.RandSync.Unsynced); RecreateFrameContents(); info.RefreshHead(); OnHeadSwitch?.Invoke(this); attachmentSliders.ForEach(s => OnSliderMoved?.Invoke(s, s.BarScroll)); return false; } }; listBox.ForceLayoutRecalculation(); foreach (var childLayoutGroup in listBox.Content.GetAllChildren()) { childLayoutGroup.Recalculate(); } } private bool OpenHeadSelection(GUIButton button, object userData) { Identifier selectedCategory = (Identifier)userData; var info = CharacterInfo; if (info.HeadSprite == null) { DebugConsole.ThrowError($"Head Selection: the head sprite is null! Failed to open the head selection."); return false; } float characterHeightWidthRatio = info.HeadSprite.size.Y / info.HeadSprite.size.X; HeadSelectionList ??= new GUIListBox( new RectTransform( new Point(parentComponent.Rect.Width, (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f)), GUI.Canvas) { AbsoluteOffset = new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, button.Rect.Bottom) }); HeadSelectionList.Visible = true; HeadSelectionList.Content.ClearChildren(); ClearSprites(); parentComponent.RectTransform.SizeChanged += () => { if (parentComponent == null || HeadSelectionList?.RectTransform == null || button == null) { return; } HeadSelectionList.RectTransform.Resize(new Point(parentComponent.Rect.Width, (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f))); HeadSelectionList.RectTransform.AbsoluteOffset = new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, button.Rect.Bottom); }; new GUIFrame( new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.ContentBackground.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) { UserData = "outerglow", CanBeFocused = false }; GUILayoutGroup row = null; int itemsInRow = 0; ContentXElement headElement = info.Ragdoll.MainElement?.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); if (headElement == null) { DebugConsole.ThrowError($"Head Selection: the head element is null in {info.ragdoll.FileName}! Failed to open the head selection."); return false; } ContentXElement headSpriteElement = headElement.GetChildElement("sprite"); ContentPath spritePathWithTags = headSpriteElement.GetAttributeContentPath("texture"); var characterConfigElement = info.CharacterConfigElement; var heads = info.Prefab.Heads; if (heads != null) { row = null; itemsInRow = 0; foreach (var head in heads.Where(h => h.TagSet.Contains(selectedCategory))) { string spritePath = info.Prefab.ReplaceVars(spritePathWithTags.Value, head); if (!File.Exists(spritePath)) { continue; } Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); headSprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); characterSprites.Add(headSprite); if (itemsInRow >= 4 || row == null) { row = new GUILayoutGroup( new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) { UserData = head.MenuCategory, Visible = true }; itemsInRow = 0; } var btn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), row.RectTransform), style: "ListBoxElementSquare") { OutlineColor = Color.White * 0.5f, PressedColor = Color.White * 0.5f, UserData = head, OnClicked = SwitchHead, Selected = info.Head.Preset == head, Visible = true }; new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); itemsInRow++; } } return false; } private bool SwitchHead(GUIButton button, object obj) { var info = CharacterInfo; var headPreset = obj as HeadPreset; if (info.Head.Preset != headPreset) { info.Head = new HeadInfo(info, headPreset, info.Head.HairIndex, info.Head.BeardIndex, info.Head.MoustacheIndex, info.Head.FaceAttachmentIndex) { SkinColor = info.Head.SkinColor, HairColor = info.Head.HairColor, FacialHairColor = info.Head.FacialHairColor }; info.ReloadHeadAttachments(); } RecreateFrameContents(); OnHeadSwitch?.Invoke(this); return true; } private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) { var info = CharacterInfo; int index = (int)Math.Round(scrollBar.BarScrollValue); switch (type) { case WearableType.Beard: info.Head.BeardIndex = index; break; case WearableType.FaceAttachment: info.Head.FaceAttachmentIndex = index; break; case WearableType.Hair: info.Head.HairIndex = index; break; case WearableType.Moustache: info.Head.MoustacheIndex = index; break; default: DebugConsole.ThrowError($"Wearable type not implemented: {type}"); return false; } info.RefreshHead(); OnSliderMoved?.Invoke(scrollBar, scrollBar.BarScroll); return true; } public void Update() { if (HeadSelectionList != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(HeadSelectionList)) { HeadSelectionList.Visible = false; } } public void AddToGUIUpdateList() { HeadSelectionList?.AddToGUIUpdateList(); } private void ClearSprites() { foreach (Sprite sprite in characterSprites) { sprite.Remove(); } characterSprites.Clear(); } public void Dispose() { ClearSprites(); if (HeadSelectionList != null) { HeadSelectionList.RectTransform.Parent = null; HeadSelectionList = null; } } } } }